This is an original planning guide. It is not a vendor manual, an official standard, or legal advice. Example addresses use documentation ranges. Review every command before you paste it.
A script is a program with an environment
The first line names the interpreter. #!/bin/bash is bash. #!/bin/sh is the system's shell, which may be bash, dash, or something else, and it will not accept every bash feature. Pick one and test on the host that will run it. A script that only ran on your laptop has not been tested.
set -euo pipefail is a reasonable start for a script that should stop on an unset variable, a failed command, and a failure in the middle of a pipe. It is not magic. A command in a conditional is allowed to fail. A pipeline's earlier commands can still surprise you if you did not mean pipefail. Read the three flags as a choice you can explain, not as a banner you paste and forget.
Quote expansions. "$dir/file" survives spaces. $dir/file does not. [[ ]] is the bash test. [ ] is the older test and it splits words. Prefer [[ ]] in a bash script, and still quote the values. A variable that comes from outside the script is data, not code. Do not eval it.
Exit codes are the API. 0 is success. Anything else is a failure the caller can branch on. echo is not an exit code. A script that prints ERROR and then exits 0 has lied. The scheduling guide will run this script and believe the code. Make the code true.
A short script that checks a directory and exits honestly
The lab script looks at one directory the ticket names, confirms it exists, and exits 0. If it is missing, the script exits non-zero and prints one sentence to stderr. It does not create the directory. Creation is a different change. The template generator on this site can draft the skeleton. You still delete every feature the form added that this script does not need.
Variables are at the top: the directory, and nothing else until you need it. The directory is an absolute path. A relative path depends on the working directory cron happened to use. The scheduling guide already asked for that directory. The script should not assume $HOME.
A function is worth it when you do the same check twice. It is not worth it when the script is twelve lines. Keep the first version boring. The linter on this site can point at unquoted expansions and at syntax the shell will reject. It does not know your intent. A clean lint and a wrong exit code is still a broken script.
Run it as the user the timer will use. Run it from a directory that is not the one in the script, so a relative path fails now. Run it once with the directory present and once with the name wrong. Keep both exit codes.
#!/bin/bash
# Review before you paste. Lab script. Exits non-zero if the directory is missing.
set -euo pipefail
dir=/srv/www/edge
if [[ ! -d "$dir" ]]; then
echo "missing $dir" >&2
exit 1
fi
exit 0
A change you can finish
Put the script in a path the service user can execute, with a mode from the permissions guide, usually 755 and not a world-writable directory. The first scheduled run is a supervised one. You watch it.
If the script must change data, the ticket names the files and the rollback. A check script does not grow a delete command in the same edit.
- Name bash or sh on the first line and test that binary on the target host.
- Turn on the failure flags you can explain.
- Quote every expansion that is a path or a value.
- Exit non-zero on the failure you care about, and say so on stderr.
- Run as the scheduled user from a neutral working directory.
- Run the success case and the failure case and keep both codes.
- Lint the file, then ignore lint hits that are not this script's bug, on purpose.
Mistakes that outlive the command
a bash script fails in the same few ways. Each one looks finished on the day you type it. The list is here so the ticket can forbid them before the window, not so you can recognise them in an incident review.
- eval of a variable, or unquoted expansion, so a space or a star becomes a command.
- #!/bin/sh and a bash-only feature such as [[, which fails on dash.
- Printing an error and exiting 0.
- Relying on aliases or on a home directory the service user does not have.
- A world-writable script. Anyone who can edit it can change what root runs, if root runs it.
- set -e and a command that fails inside a context you did not test, so the script stops halfway through a change.
- Parsing ls, or parsing the output of a command that was meant for a terminal.
- Growing a maintenance script into a second programming language no one else can read.
Decisions to write down before the window
One decision for a bash script: The shebang is bash or sh, and you ran that binary on the host that will schedule the script. Expansions are quoted. There is no eval. External input is data. The exit code matches the sentence in the header comment. Success is 0. The failure you named is non-zero. Paths are absolute. The working directory is irrelevant, and you proved that. The mode is 755 or tighter, and the directory is not world-writable. The permissions guide is the source of the numbers. The scheduled user can execute it. You ran it as that user, not only as yourself. A check stays a check. Changes to data are a separate edit with a rollback. set -euo pipefail is on because you want those failures, and you tested a missing variable and a failed command. The template from the generator was trimmed to this job. The linter output was read. Remaining warnings are either fixed or explained in one line. The script's directory is on the host that will run it, not only on your laptop. You opened that path in a shell on the host. And stderr and stdout are not mixed in a way that hides the error. The failure sentence went to stderr on purpose.
Script checks before the timer is allowed to run it
Confirm this before you call the change done: The shebang matches the host's shell test. A quoted path with a space would survive, or the path cannot contain a space and the ticket says so. The success run exited 0. The failure run exited non-zero and printed one line to stderr. The service user could execute the file. The file is not world-writable. No eval and no unquoted external value. The generator template does not still contain a sample command you do not want. cron or the timer points at this path and this user. A second person read the exit codes. The script does not print secrets. Rollback is the previous file, which you kept, or the absence of the script if it is new. A dry reading of the script by a second person found the directory name and the exit codes without asking you to explain the shebang. The hostname in the prompt matches the hostname in the ticket for a bash script. The result line includes the time and the person, not only the command. You can point at the rollback value without searching a chat history. And done is a sentence a second person can repeat: the proof for a bash script happened, and the next host is not started early. If one of those fails, stop.
Tools for this guide
These tools run in the browser on this site. They do not log into your machines.
Related pillar guides
Guides in this cluster
These supporting pages belong to this pillar.
- Checklist: Bash Script Best Practices
- How-To: Parsing Arguments in Bash
- Explainer: Bash Conditionals and Loops
- Cheat Sheet: Bash Strings and Arrays Cheat Sheet
- How-To: jq Tutorial for Admins
- How-To: yq Tutorial
- How-To: awk and sed by Example
- Explainer: Regular Expressions for Sysadmins
- Explainer: Heredocs Explained
- Explainer: Exit Codes Explained
- How-To: Customizing Your Shell (PS1, Aliases, Colors)
- Comparison: PowerShell vs Bash