A shell script is a plain text file containing shell commands that a command interpreter, such as Bash, Zsh or a POSIX sh, executes in sequence. On Linux and other Unix-like systems, shell scripts are the most common way to automate administration tasks, glue programs together and capture a repeatable procedure in a single executable file.
A shell script uses the same language a user types interactively at the terminal, extended with variables, conditions, loops and functions. Its strength is direct access to every command-line program on the system; its weakness is a language with subtle quoting and error-handling rules that make larger programs fragile.
How a shell script is executed
A script usually begins with a shebang line, such as #!/bin/bash or #!/usr/bin/env bash, which tells the kernel which interpreter to start when the file is executed directly. The file must have execute permission, set with chmod +x, and is then run by path, for example ./backup.sh, or by name if its directory is listed in the PATH variable.
The way a script is started determines its environment. Running it as a program creates a child process with a copy of exported environment variables, so changes to variables or the working directory do not affect the calling shell. Sourcing the file with source or . runs it inside the current shell, which is how configuration files such as .bashrc work.
| Method | Example | Effect |
|---|---|---|
| Direct execution | ./deploy.sh | New process, interpreter chosen by the shebang |
| Explicit interpreter | bash deploy.sh | New process, shebang ignored, no execute bit needed |
| Sourcing | source env.sh | Runs in the current shell and changes its state |
| Scheduled run | cron or a systemd timer | Runs unattended with a minimal environment |
Core language elements
Shell scripts are built from a small set of constructs. Every command returns an exit status, where 0 means success and any value from 1 to 255 signals failure; conditions and error handling are built on these codes rather than on Boolean values.
| Element | Example syntax | Purpose |
|---|---|---|
| Variable | name=value, used as "$name" | Stores text; no spaces around the equals sign |
| Positional parameters | $1, $2, "$@" | Access arguments passed to the script |
| Exit status | $? | Result of the last command |
| Conditional | if [ -f "$file" ]; then ... fi | Branches on tests or command results |
| Loop | for f in *.log; do ... done | Repeats commands over items |
| Function | cleanup() { ... } | Groups reusable commands |
| Pipeline | grep error app.log | wc -l | Passes output of one command to another |
| Command substitution | today=$(date +%F) | Captures command output in a variable |
Correct quoting is the single most important habit. An unquoted variable undergoes word splitting and filename expansion, so a filename containing a space becomes two arguments. Writing "$var" and "$@" prevents a large class of bugs.
Shell dialects and portability
Not every shell understands the same syntax. The POSIX shell standard defines a common core, while Bash and Zsh add arrays, extended tests and other conveniences. A script that uses these extensions must declare the right interpreter in its shebang.
| Shell | Typical role | Scripting notes |
|---|---|---|
| POSIX sh (dash) | System scripts on Debian and Ubuntu | Fast and minimal, no arrays or double brackets |
| Bash | Default scripting shell on most distributions | Arrays, [[ ]] tests, rich parameter expansion |
| Zsh | Interactive shell, default on macOS | Powerful but different array and globbing rules |
| BusyBox ash | Minimal containers and embedded systems | Close to POSIX with limited extras |
A frequent error is writing Bash syntax under a #!/bin/sh shebang. On systems where sh points to dash, such scripts fail with syntax errors that never appeared on the developer’s machine.
Writing robust shell scripts
By default, a shell script continues after a failed command, which can turn a small error into data loss. Defensive settings and conventions make scripts safer and easier to maintain.
- Use strict mode with
set -euo pipefailin Bash to stop on errors, unset variables and failed pipeline stages. - Clean up with traps, for example
trap cleanup EXIT, so temporary files are removed even on failure. - Check scripts with a linter such as ShellCheck, which catches quoting and portability mistakes.
- Validate arguments at the start and print a usage message when they are missing.
- Use mktemp for temporary files instead of fixed names in shared directories.
Strict mode has its own edge cases: commands inside conditions do not trigger set -e, and some commands legitimately return non-zero codes. Understanding these exceptions is part of writing reliable automation.
When a shell script is the wrong tool
Shell scripts excel at orchestrating existing programs but become hard to maintain as logic grows. The table helps decide when to switch to another tool.
| Task | Better choice | Reason |
|---|---|---|
| Running a few commands in order | Shell script | Shortest and most direct |
| Complex data structures or JSON processing | Python or another general language | Real types and libraries |
| Configuring many servers consistently | Configuration management tool | Idempotency and inventory handling |
| Script exceeding a few hundred lines | General-purpose language | Testing and error handling scale better |
| Container startup logic | Short shell entrypoint | Minimal dependencies in the image |
A practical rule used by many teams is to rewrite a script in a general-purpose language once it needs nested data structures, extensive argument parsing or unit tests. Until that point, a short, well-quoted and linted shell script remains the fastest way to automate routine work on a Linux system.
The idea in brief
A shell script is a saved list of commands for a computer. Instead of typing the same ten commands every morning, a person writes them into a file once. After that, one short command runs the whole list.
It works like a recipe card. The steps are written down, followed in order and give the same result every time. The main benefit is reliable repetition without manual typing mistakes.
A practical scenario
A system administrator must back up a web application’s database and uploaded files every night. She writes a Bash script of about 40 lines that dumps the database, compresses the upload directory into an archive named with the current date, copies both files to a backup server and deletes local archives older than 14 days.
The script uses set -euo pipefail, a cleanup trap and a final check that sends an alert if any step fails. A systemd timer runs it at 02:30 each night. When a disk later fills up, the script stops at the compression step and the alert arrives within minutes, so no incomplete backup is mistaken for a good one.