Bash scripts turn command-line commands into small programs. This guide covers the basics: variables, arguments, functions, conditions, loops, and exit codes.
Bash is the Toyota Corolla of programming languages. It is not glamorous, but it is already on most servers and gets everyday jobs done. If the command line is new to you, the beginner's guide to the Unix-style command line covers the surrounding shell basics first.
Your first script
Create a file named hello.sh:
#!/usr/bin/env bash
name=${1:-world}
printf 'Hello, %s!\n' "$name"Make it executable and run it:
$ chmod +x hello.sh
$ ./hello.sh Bob
Hello, Bob!You can also pass the script directly to Bash. This does not require executable permissions:
$ bash hello.sh Bob
Hello, Bob!${1:-world} means “use the first argument, or world if it is missing.”
The shebang
The first line is called the shebang:
#!/usr/bin/env bashIt tells the operating system to run the file with Bash. It must be the first line in the file.
You may also see #!/bin/bash. That uses Bash at one exact path.
#!/usr/bin/env bash finds Bash through your PATH, which is often more
portable.
Script arguments
Bash numbers arguments from left to right:
#!/usr/bin/env bash
printf 'Script: %s\n' "$0"
printf 'First: %s\n' "$1"
printf 'Second: %s\n' "$2"
printf 'Count: %s\n' "$#"$0is the script name$1,$2, and so on are arguments$#is the number of arguments"$@"expands to all arguments while keeping each one separate
Use "$@" when passing arguments to another command or looping over them:
for argument in "$@"; do
printf 'Argument: %s\n' "$argument"
doneQuoting matters. If an argument is Good morning, "$@" keeps it as one
argument instead of splitting it into two words.
Variables
Assign variables without spaces around =:
greeting="Hello"
name="Ada Lovelace"
printf '%s, %s!\n' "$greeting" "$name"Write "$name" to read a variable. Double quotes prevent spaces and wildcard
characters in its value from being treated as shell syntax.
Use braces when text touches the variable name:
file_name="photo"
printf '%s\n' "images/${file_name}.jpg"Use readonly for a value that should not change:
readonly backup_dir="/var/backups"By convention, environment variables and constants often use uppercase names. Regular script variables usually use lowercase names.
Functions
Define a function, then call it by name:
say_hello() {
printf 'Hello!\n'
}
say_helloFunctions receive arguments the same way scripts do:
say_hello() {
local name=$1
printf 'Hello, %s!\n' "$name"
}
say_hello "Ada Lovelace"local keeps the variable inside the function. This avoids accidentally
changing a variable elsewhere in the script.
Getting a value from a function
In Bash, return sets a function's exit status. It does not return a string.
Print the value and capture it with command substitution instead:
make_greeting() {
local name=$1
printf 'Hello, %s!' "$name"
}
greeting=$(make_greeting "Ada")
printf '%s\n' "$greeting"Use return 0 for success and a non-zero status for failure:
is_directory() {
[[ -d $1 ]]
}
if is_directory "images"; then
printf 'The images directory exists\n'
fiConditions
For Bash scripts, [[ ... ]] is the clearest way to test strings and files:
if [[ -z $name ]]; then
printf 'Name is empty\n'
elif [[ $name == "Ada" ]]; then
printf 'Hello, Ada!\n'
else
printf 'Hello, %s!\n' "$name"
fiSome useful tests:
| Test | Meaning |
|---|---|
[[ -z $value ]] | The string is empty |
[[ -n $value ]] | The string is not empty |
[[ $a == $b ]] | The strings are equal |
[[ $a != $b ]] | The strings are different |
[[ -e $path ]] | The path exists |
[[ -f $path ]] | The path is a regular file |
[[ -d $path ]] | The path is a directory |
[[ -r $path ]] | The path is readable |
[[ -w $path ]] | The path is writable |
[[ -x $path ]] | The path is executable |
(( count > 10 )) | The numeric comparison is true |
Combine tests with && for “and” and || for “or”:
if [[ -n $name && -d $backup_dir ]]; then
printf 'Ready to back up %s\n' "$name"
fiUse ! to negate a test:
if [[ ! -f $config_file ]]; then
printf 'Config file not found: %s\n' "$config_file" >&2
fi>&2 sends the message to standard error, which is where errors belong.
Loops
A for loop works well for a known list of values:
for name in Ada Grace Linus; do
printf 'Hello, %s!\n' "$name"
doneA while loop repeats as long as its condition succeeds:
count=1
while (( count <= 3 )); do
printf 'Count: %d\n' "$count"
((count += 1))
doneArrays
Bash arrays hold ordered lists. When you need text keys instead of numeric positions, Bash associative arrays are the next step:
names=("Ada Lovelace" "Grace Hopper" "Linus Torvalds")
printf 'First: %s\n' "${names[0]}"
for name in "${names[@]}"; do
printf 'Hello, %s!\n' "$name"
doneLike "$@", "${names[@]}" keeps every item separate.
Choosing with case
case is easier to read than a long chain of string comparisons:
case ${1:-} in
start)
printf 'Starting\n'
;;
stop)
printf 'Stopping\n'
;;
*)
printf 'Usage: %s {start|stop}\n' "$0" >&2
exit 1
;;
esac${1:-} safely expands to an empty string when no first argument exists.
Exit codes
Every command ends with a numeric status. Zero means success; any other value
means failure. The script's status is the status of its last command unless you
use exit explicitly.
if [[ $# -ne 2 ]]; then
printf 'Usage: %s SOURCE DESTINATION\n' "$0" >&2
exit 1
fiYou can inspect the previous command's status with $?, but testing the command
directly is usually clearer:
if cp "$1" "$2"; then
printf 'Copy complete\n'
else
printf 'Copy failed\n' >&2
exit 1
fiA small complete script
This script checks its arguments and creates a backup of one file:
#!/usr/bin/env bash
if [[ $# -ne 2 ]]; then
printf 'Usage: %s FILE BACKUP_DIR\n' "$0" >&2
exit 1
fi
file=$1
backup_dir=$2
if [[ ! -f $file ]]; then
printf 'File not found: %s\n' "$file" >&2
exit 1
fi
if ! mkdir -p "$backup_dir"; then
printf 'Could not create directory: %s\n' "$backup_dir" >&2
exit 1
fi
if ! cp "$file" "$backup_dir/"; then
printf 'Copy failed\n' >&2
exit 1
fi
printf 'Backed up %s to %s\n' "$file" "$backup_dir"That is enough Bash for many everyday scripts. Keep commands simple, quote variable expansions, and treat non-zero exit codes as failures.