Практичные инструменты для системных, сетевых, security- и operations-администраторов.

SYS-C0050 · Shell Scripting & Text Processing

Bash Scripting Basics

Start a bash script with a known shell, quoted variables, and an exit code the caller can trust.

Updated 25 October 2026 · Pillar guide

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.

  1. Name bash or sh on the first line and test that binary on the target host.
  2. Turn on the failure flags you can explain.
  3. Quote every expansion that is a path or a value.
  4. Exit non-zero on the failure you care about, and say so on stderr.
  5. Run as the scheduled user from a neutral working directory.
  6. Run the success case and the failure case and keep both codes.
  7. 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.

Decisions to write down before the window

Decision 1 for a bash script. The shebang is bash or sh, and you ran that binary on the host that will schedule the script. Write that decision in the ticket before you change the host. The proof is the smallest command or screen that confirms it, recorded with the hostname and the time. If the host disagrees, the ticket changes. A second change does not go on top of a failed check. The rollback is the previous value you wrote down, not a reboot that hides which line mattered.

Decision 2 for a bash script. Expansions are quoted. There is no eval. External input is data. Write that decision in the ticket before you change the host. The proof is the smallest command or screen that confirms it, recorded with the hostname and the time. If the host disagrees, the ticket changes. A second change does not go on top of a failed check. The rollback is the previous value you wrote down, not a reboot that hides which line mattered.

Decision 3 for a bash script. The exit code matches the sentence in the header comment. Success is 0. The failure you named is non-zero. Write that decision in the ticket before you change the host. The proof is the smallest command or screen that confirms it, recorded with the hostname and the time. If the host disagrees, the ticket changes. A second change does not go on top of a failed check. The rollback is the previous value you wrote down, not a reboot that hides which line mattered.

Decision 4 for a bash script. Paths are absolute. The working directory is irrelevant, and you proved that. Write that decision in the ticket before you change the host. The proof is the smallest command or screen that confirms it, recorded with the hostname and the time. If the host disagrees, the ticket changes. A second change does not go on top of a failed check. The rollback is the previous value you wrote down, not a reboot that hides which line mattered.

Decision 5 for a bash script. The mode is 755 or tighter, and the directory is not world-writable. The permissions guide is the source of the numbers. Write that decision in the ticket before you change the host. The proof is the smallest command or screen that confirms it, recorded with the hostname and the time. If the host disagrees, the ticket changes. A second change does not go on top of a failed check. The rollback is the previous value you wrote down, not a reboot that hides which line mattered.

Decision 6 for a bash script. The scheduled user can execute it. You ran it as that user, not only as yourself. Write that decision in the ticket before you change the host. The proof is the smallest command or screen that confirms it, recorded with the hostname and the time. If the host disagrees, the ticket changes. A second change does not go on top of a failed check. The rollback is the previous value you wrote down, not a reboot that hides which line mattered.

Decision 7 for a bash script. A check stays a check. Changes to data are a separate edit with a rollback. Write that decision in the ticket before you change the host. The proof is the smallest command or screen that confirms it, recorded with the hostname and the time. If the host disagrees, the ticket changes. A second change does not go on top of a failed check. The rollback is the previous value you wrote down, not a reboot that hides which line mattered.

Decision 8 for a bash script. set -euo pipefail is on because you want those failures, and you tested a missing variable and a failed command. Write that decision in the ticket before you change the host. The proof is the smallest command or screen that confirms it, recorded with the hostname and the time. If the host disagrees, the ticket changes. A second change does not go on top of a failed check. The rollback is the previous value you wrote down, not a reboot that hides which line mattered.

Decision 9 for a bash script. The template from the generator was trimmed to this job. Write that decision in the ticket before you change the host. The proof is the smallest command or screen that confirms it, recorded with the hostname and the time. If the host disagrees, the ticket changes. A second change does not go on top of a failed check. The rollback is the previous value you wrote down, not a reboot that hides which line mattered.

Decision 10 for a bash script. The linter output was read. Remaining warnings are either fixed or explained in one line. Write that decision in the ticket before you change the host. The proof is the smallest command or screen that confirms it, recorded with the hostname and the time. If the host disagrees, the ticket changes. A second change does not go on top of a failed check. The rollback is the previous value you wrote down, not a reboot that hides which line mattered.

Decision 11 for a bash script. 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. Write that decision in the ticket before you change the host. The proof is the smallest command or screen that confirms it, recorded with the hostname and the time. If the host disagrees, the ticket changes. A second change does not go on top of a failed check. The rollback is the previous value you wrote down, not a reboot that hides which line mattered.

Decision 12 for a bash script. stderr and stdout are not mixed in a way that hides the error. The failure sentence went to stderr on purpose. Write that decision in the ticket before you change the host. The proof is the smallest command or screen that confirms it, recorded with the hostname and the time. If the host disagrees, the ticket changes. A second change does not go on top of a failed check. The rollback is the previous value you wrote down, not a reboot that hides which line mattered.

Script checks before the timer is allowed to run it

Check 1 in Script checks before the timer is allowed to run it. The shebang matches the host's shell test. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 2 in Script checks before the timer is allowed to run it. A quoted path with a space would survive, or the path cannot contain a space and the ticket says so. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 3 in Script checks before the timer is allowed to run it. The success run exited 0. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 4 in Script checks before the timer is allowed to run it. The failure run exited non-zero and printed one line to stderr. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 5 in Script checks before the timer is allowed to run it. The service user could execute the file. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 6 in Script checks before the timer is allowed to run it. The file is not world-writable. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 7 in Script checks before the timer is allowed to run it. No eval and no unquoted external value. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 8 in Script checks before the timer is allowed to run it. The generator template does not still contain a sample command you do not want. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 9 in Script checks before the timer is allowed to run it. cron or the timer points at this path and this user. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 10 in Script checks before the timer is allowed to run it. A second person read the exit codes. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 11 in Script checks before the timer is allowed to run it. The script does not print secrets. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 12 in Script checks before the timer is allowed to run it. Rollback is the previous file, which you kept, or the absence of the script if it is new. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 13 in Script checks before the timer is allowed to run it. 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. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 14 in Script checks before the timer is allowed to run it. The hostname in the prompt matches the hostname in the ticket for a bash script. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 15 in Script checks before the timer is allowed to run it. The result line includes the time and the person, not only the command. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 16 in Script checks before the timer is allowed to run it. You can point at the rollback value without searching a chat history. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Check 17 in Script checks before the timer is allowed to run it. Done is a sentence a second person can repeat: the proof for a bash script happened, and the next host is not started early. Record the device name, the result, and the time on the same line. If the result does not match this check, stop and fix the ticket before you add another change. The rollback for this check is the previous value you wrote down, not a reboot. A reboot hides whether the check was the one that mattered. When the check passes, move to the next one. When it fails, the incident is this check.

Tools for this guide

These tools run in the browser on this site. They do not log into your machines.

Guides in this cluster

These supporting pages belong to this pillar. A published page is linked. The others are named until they are published.