A shell script becomes trustworthy when its behavior stays predictable outside your terminal: different directories, awkward filenames, missing tools, and interrupted runs. This sprint builds a practical safety checklist around one task: reading an input file and publishing a generated report without damaging an existing report.
1. Declare Bash and define the script’s contract
Start with #!/usr/bin/env bash if your script uses Bash features. Run it directly or with Bash, not with sh. Document the required environment, including any platform-specific utilities. A script tested on Linux should not silently promise compatibility with every macOS installation.
Define the interface before writing the transformation: exactly two arguments, an existing input file and an output path. State whether replacing an existing report is allowed. Send the report to its destination, progress messages to standard error, and return a nonzero status when the task fails.
2. Enable guardrails without outsourcing error handling
Add set -euo pipefail near the top. This catches many accidental failures, unset variables, and unsuccessful pipeline stages. It is a useful baseline, not a proof of correctness: Bash treats failures differently inside conditions, command lists, and other contexts.
Handle important failures explicitly. For example, if ! generate_report; then printf '%s\n' 'Report generation failed' >&2; exit 1; fi communicates intent better than hoping execution stops at the right place. Check required tools with command -v before creating files or changing anything.
3. Validate arguments before touching the filesystem
Check the argument count before reading positional parameters; nounset makes accessing a missing argument an error. Then assign descriptive variables such as input and output. Reject an input that is not a readable regular file, and require the output’s parent directory to exist.
Decide what should happen when input and output identify the same file, including through a symbolic link. For this report script, reject that situation. Validation improves diagnostics, but it does not eliminate filesystem races. Run sensitive jobs in directories that untrusted users cannot modify.
4. Quote expansions and keep filenames as data
Use "$input", not an unquoted variable. Unquoted expansions can split a single filename into several arguments or expand wildcard characters. Pass argument lists as Bash arrays and expand them with "${args[@]}".
Where a utility supports it, place -- before filename operands so a name beginning with a dash is not interpreted as an option. Do not parse ls output or construct commands with eval. For line-oriented input, use while IFS= read -r line || [[ -n "$line" ]] to preserve backslashes and process a final line without a newline.
5. Stage the report and publish only after success
Create a temporary file with mktemp in the output’s parent directory, using a template such as "$output_dir/.report.XXXXXX". Check that creation succeeds. Set a restrictive umask first if the report contains private data, and establish cleanup immediately after allocating the file.
Write the complete report to that temporary file. Verify the transformation succeeded and validate any required report structure before renaming it into place. Keeping the temporary file on the same filesystem allows a rename to provide atomic visibility: readers see the previous report or the completed replacement, not half-written content.
Install an EXIT trap that removes the temporary file and preserves the original exit status. Handle INT and TERM by exiting so cleanup runs. Remember that traps cannot recover from every event, including forced termination. If concurrent writers are possible, add a documented locking strategy.
6. Test failures as deliberately as successful runs
Run bash -n for syntax checks and ShellCheck for suspicious constructs. Then test in a disposable directory rather than against production files.
- Use filenames containing spaces, wildcard characters, and leading dashes.
- Try missing arguments, unreadable input, and an unwritable destination.
- Force the generator to fail after writing partial output.
- Interrupt execution and confirm temporary files are cleaned up.
- Verify that every failed run leaves the previous report unchanged.
7. Review the script before granting real access
Ask a teammate or coding assistant to inspect quoting, failure paths, cleanup, and destructive operations. SwiftRelay works with Claude Code, Codex, and Gemini CLI, making it a practical option for an additional review pass. Sign up for SwiftRelay if you want to use that workflow.
Share a sanitized script and synthetic fixtures, never credentials or private reports. Treat suggested edits as untrusted changes: inspect the diff, rerun the failure tests, and grant production access only after the script passes its contract.