Bash basics — commands, quoting, pipes, variables, and safe scripts
Shell model
Bash reads command language and starts programs
A terminal is the interface where text is displayed and typed. A shell is the program that interprets command lines. Bash is one shell implementation; others include zsh, dash, and fish. A command that works interactively in one shell may fail in a script executed by another, so the script shebang should name the language it uses.
Bash has built-in commands such as cd, read, and export, and it can start external programs found through PATH. Use type command-name to learn whether a name is an alias, function, builtin, or executable file.
type cd
type printf
type grep
command -v bash
bash --version
Commands and status
Every command returns an exit status
An exit status of zero means success; a nonzero status reports some form of failure. The special parameter $? contains the status of the most recently completed foreground command, but it changes as soon as another command runs. Prefer testing a command directly in an if statement when the goal is to branch on success.
if grep -q 'ready' app.log; then
printf '%s
' 'application is ready'
else
printf '%s
' 'ready marker not found' >&2
fi
Quoting and expansion
Quote variable expansions unless splitting is intentional
Bash expands parameters, command substitutions, arithmetic, and wildcard patterns before it starts the target program. Unquoted results can be split on whitespace and expanded as filename patterns. Double quotes preserve each expanded value as one argument while still allowing variables and command substitution. Single quotes preserve text literally.
Use an array when several distinct arguments must be stored. A quoted scalar is one argument; an unquoted scalar is not a safe substitute for an argument list.
"$value"passes one argument, even when the value is empty or contains spaces.'$value'passes the literal characters$value."$(command)"captures output without splitting it into words."${array[@]}"expands an array as one argument per element.
file='Quarterly Report.txt'
printf '%s
' "$file"
options=(--format json --output "build report.json")
my-command "${options[@]}"
Pipes and redirection
Pipes connect processes; redirections connect file descriptors
A pipe sends the standard output of one command to the standard input of the next. Standard error remains separate unless redirected. Redirections are processed by the shell before a program starts, and their order matters because 2>&1 duplicates the destination that standard output has at that moment.
# stdout to a file, stderr to another file
my-command >output.log 2>error.log
# both streams to one file
my-command >combined.log 2>&1
# filter stdout while preserving pipeline failure status
set -o pipefail
my-command | grep 'important'
Variables and environment
Shell variables become environment variables only when exported
A Bash variable belongs to the current shell. export marks it for inclusion in the environment inherited by child processes. Child processes cannot modify the parent shell environment, which is why running a script cannot permanently change the caller directory or variables unless the file is sourced.
Prefer lowercase names for script-local variables and reserve uppercase names for exported configuration and established environment variables. Use readonly for values that must not change after initialization.
api_url='http://127.0.0.1:3000'
export APP_ENV='development'
readonly config_file='./app.conf'
env | grep '^APP_ENV='
Script structure
Validate inputs and make failure behavior explicit
set -u catches unset variables, and set -o pipefail exposes failures earlier in a pipeline. set -e can be useful but has exceptions around conditions, lists, and subshells; it does not replace explicit checks for expected failures. Add a cleanup trap only for resources the script actually created.
#!/usr/bin/env bash
set -u -o pipefail
usage() { printf 'Usage: %s FILE
' "$0" >&2; }
if (( $# != 1 )); then
usage
exit 64
fi
input_file=$1
[[ -f $input_file ]] || { printf 'Not a file: %s
' "$input_file" >&2; exit 66; }
Debugging
Check syntax, then trace expansions and commands
Run the script with the same Bash version and environment used in production. bash -n parses without executing. bash -x prints expanded commands before execution; direct trace output to a protected file if commands may contain credentials. ShellCheck is also valuable for quoting, portability, and common logic errors.
- Syntax error: inspect the line before the reported location for an unclosed quote, bracket, or command substitution.
- Command not found: inspect spelling,
PATH, aliases, and the script interpreter. - Permission denied: inspect file mode, directory traversal permission, mount options, and the shebang interpreter.
- Unexpected empty value: trace where the variable is assigned and whether a subshell changed its scope.
bash -n script.sh
BASH_XTRACEFD=3 bash -x script.sh 3>trace.log
shellcheck script.sh
Portability
Choose Bash features deliberately
Arrays, [[ ]], and several expansion forms are Bash features and are not guaranteed in POSIX sh. Use a Bash shebang when they make the script clearer. If the script must run under /bin/sh across minimal systems, test with the actual shell implementation and avoid Bash-only syntax.
Related