Bash basics — commands, quoting, pipes, variables, and safe scripts

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

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

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.

file='Quarterly Report.txt'
printf '%s
' "$file"

options=(--format json --output "build report.json")
my-command "${options[@]}"

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'

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='

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; }

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.

bash -n script.sh
BASH_XTRACEFD=3 bash -x script.sh 3>trace.log
shellcheck script.sh

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.