Pipes and text
Introduction to Bash Shell Scripting
Learn Bash scripting with executable files, variables, quoting, conditions, loops, functions, arrays, positional arguments, and getopts.
8 minute lesson
Bash scripts put shell commands into a file so you can repeat a task.
Bash is both a command interpreter and a programming language. It has variables, conditions, loops, arrays, functions, and input/output redirection.
Shell scripts are excellent for connecting command-line tools. When a script grows into a large application, another language may be easier to test and maintain.
Check out my introduction to Bash first if the shell is new to you.
Create a script
Create a file named hello.sh:
#!/usr/bin/env bash
echo "Hello from Bash"
The first line is the shebang. It asks env to find bash in the current PATH.
Use an exact path such as #!/bin/bash when the environment guarantees that location and you want that specific interpreter.
Make the file executable:
chmod u+x hello.sh
Run it:
./hello.sh
Alternatively, pass the file to Bash:
bash hello.sh
In that form, Bash is already the interpreter and does not use the shebang to choose one.
Check syntax without running the script:
bash -n hello.sh
Add comments
A comment starts with #:
#!/usr/bin/env bash
# Print a greeting
echo "Hello"
The shebang is the special first-line exception.
Create variables
Assign a value without spaces around =:
name="Flavio"
count=3
Read a variable with $:
echo "$name"
echo "$count"
Quote variable expansions unless you specifically need word splitting or filename expansion:
file_name="My notes.txt"
cat "$file_name"
Without quotes, Bash can split one value into several arguments and expand wildcard characters.
Environment variables are inherited by child processes only when exported:
export APP_ENV="production"
Capture command output
Use command substitution:
current_date=$(date +%F)
echo "Today is $current_date"
$(...) is easier to nest and read than the older backtick syntax.
Work with exit statuses
Every command returns an exit status.
0 means success. A nonzero value means failure.
Run a second command only when the first succeeds:
mkdir -p backup && cp notes.txt backup/
Run a fallback when the first command fails:
cp notes.txt backup/ || echo "The copy failed" >&2
Negate a command’s status with !:
if ! grep -q "ready" status.txt
then
echo "Not ready"
fi
Write conditions
Use [[ ... ]] for Bash string and file tests:
dog_name="Roger"
if [[ -z $dog_name ]]
then
echo "A name is required"
else
echo "The dog is $dog_name"
fi
-z tests for an empty string. Use -n for a non-empty string.
Compare strings:
if [[ $dog_name == "Roger" ]]
then
echo "Found Roger"
fi
Test files:
if [[ -f notes.txt ]]
then
echo "notes.txt is a regular file"
fi
Common file tests include:
-eexists-fis a regular file-dis a directory-ris readable-wis writable-xis executable
Use (( ... )) for integer arithmetic:
age=23
minimum=18
if (( age >= minimum ))
then
echo "Old enough"
fi
Inside arithmetic expressions, use operators such as +, -, *, /, %, <, <=, ==, >=, and >.
The test command and [ ... ] also work, including in POSIX shell scripts. Their parsing rules differ from [[ ... ]], so do not mix the syntaxes blindly.
Use if, elif, and else
The complete shape is:
if command_one
then
echo "First condition matched"
elif command_two
then
echo "Second condition matched"
else
echo "No condition matched"
fi
The commands after if and elif are tested by exit status.
Loop over values
Use for to iterate over a list:
for city in Copenhagen Rome Lisbon
do
echo "$city"
done
Loop over script arguments without losing their boundaries:
for argument in "$@"
do
echo "$argument"
done
"$@" expands to one quoted word per argument. Prefer it over $*.
Use while while a command succeeds:
count=1
while (( count <= 3 ))
do
echo "$count"
((count += 1))
done
Use break to leave a loop and continue to skip to its next iteration.
Match values with case
case is clear when one value can match several patterns:
case $1 in
start)
echo "Starting"
;;
stop)
echo "Stopping"
;;
restart|reload)
echo "Restarting"
;;
*)
echo "Usage: $0 {start|stop|restart}" >&2
exit 2
;;
esac
The *) branch is the fallback.
Read input
Use read -r so backslashes are not treated as escapes:
read -r -p "Your name: " name
echo "Hello $name"
Check whether input succeeded when a script can receive end-of-file:
if IFS= read -r line
then
echo "$line"
fi
Setting IFS= preserves leading and trailing whitespace.
Use positional arguments
Bash exposes script arguments as:
$0: the script name$1,$2, and so on: positional arguments$#: the number of positional arguments"$@": every positional argument, preserving boundaries
Example:
#!/usr/bin/env bash
if (( $# != 1 ))
then
echo "Usage: $0 FILE" >&2
exit 2
fi
file=$1
echo "Processing $file"
Use ${10} for argument ten and higher.
Parse options with getopts
The Bash getopts builtin parses short options:
verbose=false
name=""
while getopts ":vn:" option
do
case $option in
v)
verbose=true
;;
n)
name=$OPTARG
;;
:)
echo "Option -$OPTARG needs a value" >&2
exit 2
;;
\?)
echo "Unknown option: -$OPTARG" >&2
exit 2
;;
esac
done
shift "$((OPTIND - 1))"
The option name goes into option. An option’s value goes into OPTARG.
The colon after n in :vn: means -n needs a value. The leading colon lets the script handle errors itself.
After shift, the remaining positional arguments start at $1.
Use -- when calling a command to mark the end of options:
./report.sh -v -- -july.csv
The single hyphen in the old tutorial was incorrect.
Create arrays
Create an indexed array:
breeds=(
"husky"
"setter"
"border collie"
)
Read one element:
echo "${breeds[0]}"
Loop over every element:
for breed in "${breeds[@]}"
do
echo "$breed"
done
Get the number of elements:
echo "${#breeds[@]}"
Always include the parameter expansion syntax. Writing breeds[0] by itself does not read the value.
Create functions
Define a function:
clean_folder() {
local folder=$1
echo "Cleaning $folder"
}
Call it like another command:
clean_folder "/Users/flavio/Desktop"
Function arguments use $1, $2, and "$@".
Variables are not all global. Use the Bash local builtin to keep a function variable inside that function.
Return a status with return:
is_text_file() {
[[ $1 == *.txt ]]
}
Bash functions return an integer status, not an arbitrary string. Print data to standard output when a caller needs to capture it.
Handle failures
Check failures where you can explain or recover from them:
if ! cp "$source" "$destination"
then
echo "Could not copy $source" >&2
exit 1
fi
Many scripts enable stricter behavior with:
set -u
set -o pipefail
set -u reports unset variables. pipefail makes a pipeline fail when any command in it fails.
You will also see set -e. Its behavior has important exceptions around conditions, pipelines, subshells, and command lists. Do not add it without understanding how the script handles expected failures.
For the complete language rules and builtin documentation, see the official GNU Bash Reference Manual.
Lesson completed