Bash is both an interactive command language and a scripting runtime. Reliable shell code treats words, exit statuses, and file descriptors as distinct data flows. Start scripts with an explicit interpreter when Bash features are required:
#!/usr/bin/env bashbash -c 'commands' name arg1 assigns name to $0 and arg1 to $1.
That detail matters when a command string consumes positional parameters.
Shell Grammar and Execution #
A list joins pipelines with ;, &, &&, or ||. && and || are
short-circuit operators; & starts an asynchronous list. A pipeline joins
commands with |, or with |& when the left command’s standard error is also
sent to the pipe.
build && test && deploy
generate_report 2>&1 | tee build.logParentheses create a subshell, so assignments, directory changes, and options inside them do not affect the calling shell. Braces execute in the current shell and require spaces and a terminating semicolon or newline.
( cd build && make )
{ cd build && make; }Commands in a pipeline execute in separate environments by default. Therefore,
do not expect a variable assigned by a while loop on the right of a pipeline
to remain set afterward. Feed the loop with redirection or process
substitution instead:
count=0
while IFS= read -r line; do
((count++))
done < <(printf '%s\n' one two)
printf '%s\n' "$count"shopt -s lastpipe changes this for the final pipeline command under specific
non-interactive, job-control-disabled conditions; code should not depend on
that optional execution model.
Conditions, Loops, and Functions #
Shell conditions are command statuses: zero succeeds and nonzero fails.
if, while, and until accept a list, not a Boolean expression.
if [[ -d $directory && -r $directory/config ]]; then
printf 'configuration is readable\n'
elif [[ -e $directory ]]; then
printf 'configuration is unavailable\n' >&2
else
printf 'directory does not exist\n' >&2
fiUse [[ ... ]] for Bash conditionals because it avoids word splitting and
pathname expansion on unquoted operands. Use (( ... )) for arithmetic and
case for glob-pattern dispatch. Quote variable expansions in commands even
when a surrounding conditional does not require it.
for file in "$@"; do
[[ -f $file ]] || continue
process "$file"
done
retry=0
until fetch; do
((++retry >= 3)) && exit 1
doneFunctions run in the current shell. local limits a variable to the current
function; return supplies its status. Arguments are the function’s
positional parameters, so forward them with "$@".
run_checked() {
local output
output=$("$@") || return
printf '%s\n' "$output"
}Expansion and Quoting #
Before a command executes, Bash performs brace expansion; tilde expansion; parameter, arithmetic, command, and process substitution; word splitting; pathname expansion; and quote removal. Parameter, arithmetic, command, and process substitutions occur left to right. Brace expansion occurs before parameter expansion, so it cannot use a variable to create a range.
Quoting changes this process:
- Single quotes preserve every character until the next single quote.
- Double quotes preserve one word while allowing parameter, arithmetic, and command substitution.
- Unquoted expansions undergo word splitting and pathname expansion.
"$@"expands to one word per positional parameter;"$*"expands to one word joined with the first character ofIFS.
files=("$directory"/*.log)
printf '%s\n' "${files[@]}"
name=${path##*/} # Remove the longest matching prefix.
extension=${name##*.}
printf '%s\n' "${name%.*}" # Remove the shortest matching suffix.
printf '%s\n' "$((retries + 1))"An unmatched unquoted glob remains literal by default. Set nullglob only in
a tightly scoped context when an empty result is required; failglob turns an
unmatched pattern into an error. Never parse arbitrary file names with
for file in $(command) because splitting and globbing corrupt names.
Arrays, Arithmetic, and Regular Expressions #
Indexed and associative arrays preserve element boundaries only when expanded
as "${array[@]}". Declare an associative array before assigning string keys.
declare -a files=(one 'two words')
declare -A status=([build]=pending)
files+=("three words")
for file in "${files[@]}"; do
printf '<%s>\n' "$file"
doneArithmetic accepts shell variables and returns status zero when the evaluated
value is nonzero. Prefix a decimal number with 10# if input may contain
leading zeroes; otherwise Bash can interpret a leading zero as octal.
number=008
(( value = 10#$number + 1 ))[[ value =~ regex ]] uses POSIX extended regular expressions. Keep a
nonliteral pattern in a variable and leave that expansion unquoted; quotation
changes matching semantics. After a successful match, BASH_REMATCH[0]
contains the full match and later elements contain capturing groups.
version='release-1.2.3'
regex='^release-([0-9]+)\.([0-9]+)\.([0-9]+)$'
if [[ $version =~ $regex ]]; then
printf 'major=%s\n' "${BASH_REMATCH[1]}"
fiRedirection and Input #
Redirections apply left to right after the pipeline connection is established.
2>&1 duplicates the current destination of standard output, so its position
changes the result.
command >output.log 2>&1 # Both standard streams go to the file.
command 2>&1 >output.log # Standard error retains its prior destination.
command |& consumer # Equivalent to command 2>&1 | consumer.Open a dedicated descriptor when a loop needs input while a command uses standard input:
while IFS= read -r line <&3; do
printf '%s\n' "$line"
done 3< input.txtread -r prevents backslash interpretation. With no variable, read assigns
to REPLY; it removes the input delimiter. mapfile (also named readarray)
reads lines into an indexed array and retains delimiters unless -t is used.
mapfile -t lines < input.txt
printf '%s\n' "${lines[@]}"An unquoted here-document delimiter permits parameter, command, and arithmetic
expansion. A quoted delimiter makes the body literal. <<- removes leading
tab characters, not spaces.
cat <<'EOF'
literal $HOME and $(command)
EOFStartup Files, Prompts, and Arguments #
Interactive login Bash reads /etc/profile and then the first readable file
of ~/.bash_profile, ~/.bash_login, and ~/.profile. Interactive
non-login Bash reads ~/.bashrc. Distribution files can add system-wide
startup files, so use the local documentation rather than assuming a path.
Non-interactive Bash reads the file named by BASH_ENV when that variable is
set.
Place interactive aliases, completion, and prompt setup in ~/.bashrc.
Login profiles commonly source it when an interactive login shell should share
that setup. Export environment variables in a login startup file only when
child processes need them.
PS1 controls the primary interactive prompt. Wrap nonprinting terminal
sequences in \[ and \]; otherwise readline calculates the visible prompt
width incorrectly.
PS1='\[\e[32m\]\u@\h \W\[\e[0m\]\$ '$0 names the shell or script, $# is the argument count, and $1 through
${10} select arguments. Preserve argument boundaries with "$@"; use
shift only after consuming the current argument deliberately.
Failure Semantics and Options #
The status of a pipeline is the final command’s status unless set -o pipefail
is active, in which case a nonzero status is reported if any command fails.
PIPESTATUS holds every command’s status immediately after a pipeline.
set -o pipefail
producer | consumer
printf 'statuses: %s\n' "${PIPESTATUS[*]}"set -e does not mean “exit on every error.” Bash suppresses its effect in
condition tests, all but the final commands of && and || lists, commands
whose status is inverted with !, and several compound-command contexts.
Thus write error handling where it matters:
if ! deploy "$environment"; then
printf 'deployment failed\n' >&2
exit 1
fiset -u detects expansions of unset variables but requires explicit defaults
such as ${optional:-} for optional inputs. set -x exposes expanded commands
and may reveal secrets, so enable it only in controlled diagnostics. Use
set -euo pipefail only when the script understands each option’s semantics;
it supplements, rather than replaces, explicit validation and cleanup.
Comments
Read or comment on GitHub (sign-in required to comment)