Commands

Intro

The general format of a command is:

command-name args...

Specifying The Executable

Commands are just regular programs (i.e., executable files). You don’t have to specify the program’s full path because the shell looks for it in PATH, which is a environment variable (see Exporting Variables) that contains a list of directories that may contain installed binaries. So, instead of /usr/bin/ls you can just say ls, because PATH contains /usr/bin. If you ever need to add something to the PATH, put export PATH=/path/to/add:$PATH in your ~/.bashrc.

Options

You can change how a command behaves by appending options. Typically each option has a short form like -a, or a long form like --all. The short form is indicated by a single dash, and the long form by double dash. Beware that sometimes lesser-used options don’t have a short option, and short options aren’t always the first letter of the longer counterpart. You can chain short options together like so: -a -b -c becomes -abc.

Some options can even take values, like so:

foo -u root
foo --user root
foo --user=root

For -u root, you often don’t need the space between the u and the r (this depends on the program’s command-line argument parser), but it is recommended for readability. Note that when an option takes a value, other short options can still be chained with the value-taking option, as long as they come before (otherwise these options may be interpreted as a value).

Note that most commands come with a --help and a -h option, but sometimes the short option isn’t always the help message. The short option might not even be there, and vice versa. So try the long option first in case -h does something weird.

Positional Arguments

Another type of argument is positional arguments, and they don’t need dashes. You can specify them any where in the command, as long as they are in the order that the program expects. Some commands are more finicky with their positioning though, so it may be a good habit to place all options before positional arguments, like so:

foo -abc --long-opt --opt-with-arg opt-arg positional-arg-1 positional-arg-2

Shell Prompt (Interactive)

A prompt is displayed when an interactive shell is asking for the next command. Generally, a prompt ending in $ means “this command will be run as a regular user”, while a # prompt means “this command will be run as root.” On modern and user-friendly shells, the prompt might also include the current working directory, username, time, and so on. In Bash, you can configure the prompt via PROMPT_COMMAND.

Variables

Defining Variables

name=Carl
age=16

Note that Bash does not distinguish between text and numbers — they are all strings to Bash.

Substitution

To access the value in a variable, use $:

age=16
echo $age # output: 16

Arrays

To define arrays:

squares=(1 4 9 16 25 36 49 64 81 100)
random_stuff=(1 asdf hey $(pwd) waddup)

Bash arrays are zero-indexed.

echo ${squares[0]} # prints 1

To append multiple elements:

squares+=(121 144 169 196)

To get array size:

echo ${#squares[@]}

To get a list of array indices to iterate over:

echo ${!arr[@]} # 0, 1, 2, etc

To get n elements starting at index i:

echo ${squares[@]:i:n}

You can also iterate over array elements via a for loop.

Exporting Variables

export EXISTING_VARIABLE
export NEW_VARIABLE=hello

Exporting a variable makes a shell variable available as a environment variable, which makes it visible to subprocesses (like commands you run in Bash). Putting it in ~/.bashrc makes it persistent.

Basic I/O

echo: print things to the terminal

echo # print a newline
echo hello
echo spaces do not matter
echo "consecutive spaces   need quotes"
echo -n "print without newline"
echo -e "print\tspecial\ncharacters"

Also see printf.

read: read input from the user

read # by default, values are read into $REPLY
read var # read into a variable called `var`
read -p "Enter a value: " var # with prompt
read -r -p "New shell prompt: " var # interpret escape sequences

cat: concatenate files

cat file # prints file
cat file1 file2 # prints files in order

Functions

Functions are user-defined commands. There are two different but equivalent syntaxes for defining a function in Bash:

# Syntax 1
function foo {
    echo "This is foo!"
}
 
# Syntax 2
bar() {
    echo "This is bar!"
}
 
foo # output: This is foo!
bar # output: This is bar!

Accessing Function Arguments

The n-th argument can be accessed through $n. To access all arguments. To access all arguments as a string, use $*. To access all arguments as an array, use $@.

Return Value (Function Exit Code)

By default, a function returns 0 when it reaches the end. But a different value can be specified with return.

function agecheck {
    if [ "$1" -gt 18 ]; then
        echo "Admitted"
    else
        echo "Underage; not admitted"
        return 1
    fi
}

You can’t directly return text from a command, but you can print text in the function, and capture its output (e.g., foo=$(func-with-output)).

Local Variable

By default, any variable assignment is global. To make a variable local-only, prepend local when assigning.

msg="hello1"
local_var_example() {
    local msg="hello2"
    echo $msg
}
local_var_example # prints hello2
echo $msg # prints hello1

Redirection

Background

Each command/program/process has three “streams” to begin with. These streams help user interact with the program. Each stream is identified by a file descriptor (“fd”).

NamefdPurpose
stdin0Programs receive user input from stdin.
stdout1Programs write output to stdout.
stderr2Programs write error messages to stderr.

Redirecting to/from file

Read file into stdin instead of typing:

foo < input_file

Write stdout to file instead of printing on screen:

foo > output_file

Write stderr to file instead of printing on screen:

foo 2> error_file

Note: 2>/dev/null is often used to hide error messages for a command, since /dev/null is special file / data sink for undesired output. Everything that is redirected to /dev/null gets discarded.

Redirecting to existing fd

Using &, we can refer to an existing fd when redirecting (without the &, Bash would think that you gave a filename). For instance, to redirect stderr to stdout:

foo 2>&1

Redirecting stdout and stderr simultaneously

>& (alternatively, &>) allows us to redirect stdin and stderr at the same time:

foo >& /dev/null
bar &> /dev/null

This bash-only operator is equivalent to >file 2>&1.

Creating new fd

Additional fd’s can be created by simply redirecting existing streams to a new fd. The following example creates a new fd (3) to have only stdout printed on screen but both stdout and stderr is logged as well:

{ foo 2>&1 1>&3 | tee stderr.log ; } 3>&1 | tee both.log > /dev/null

Boolean Expression

Logical Operators

  • When two commands are connected with &&, the second command only executes when the first command succeeds (exit code is 0).
  • When two commands are connected with ||, the second command only executes when the first command fails (exit code is non-zero).
  • If a bash script has set -e (exit upon error), then adding || true to commands that may fail non-fatally will prevent them from aborting the script.

Commands

  • Commands can be strung together with Logical Operators and be used as conditions in If Elif Else Statements.
  • Branches can be contingent on command exit codes (like if grep postgres /etc/passwd), with 0 being true and any other value being false.
  • Negation can be achieved by prepending the condition with !.
  • Grouping can be achieved through parentheses, which will create a subshell that evaluates the commands. The value of the condition will be the exit code of the subshell.

Single Bracket

age=18
 
if [ $age -lt 21 ]; then
    echo You are not allowed to drink alcohol
fi
OperatorExpression is true when…
! EXPREXPR is false.
-n STRINGSTRING is not empty (non-zero-sized)
-z STRINGSTRING is empty (zero-sized)
STRING1 != STRING2STRING1 is not equal to STRING2
STRING1 = STRING2STRING1 is equal to STRING2
INTEGER1 -eq INTEGER2INTEGER1 is equal to INTEGER2
INTEGER1 -ne INTEGER2INTEGER1 is not equal to INTEGER2
INTEGER1 -gt INTEGER2INTEGER1 is greater than INTEGER2
INTEGER1 -lt INTEGER2INTEGER1 is less than INTEGER2
INTEGER1 -ge INTEGER2INTEGER1 is greater than or equal to INTEGER 2
INTEGER1 -le INTEGER2INTEGER1 is less than or equal to INTEGER 2
-e PATHPATH exists
-f PATHPATH exists and is a flie
-d PATHPATH exists and is a directory
-r PATHPATH exists and is readable
-s PATHPATH exists and is nonempty
-w PATHPATH exists and is writeable
-x PATHPATH exists and is executable
EXPR1 -o EXPR2at least one of EXPR1 or EXPR2 is true
EXPR1 -a EXPR2both EXPR1 and EXPR2 are true

Remember to quote your strings

Quote string operands when comparing using [ ]. Single square bracket pairs refer to the test command, which could misinterpret words in a single string argument as multiple arguments. The Bash-specific [[ ]] operator does not have this requirement.

Double Bracket

The double bracket [[ ]] is a Bash-builtin operator, and is thus not POSIX-compliant. You shouldn’t use it if you want your script to be cross-platform (a “shell script” instead of “bash script”), but otherwise it’s pretty handy in bash scripts, since you don’t need to quote strings at all (e.g., for empty or multi-word strings) and you have access to more readable boolean operators (||, &&, !).

Control Flow

If/Elif/Else Statement

if condition; then
    # ...
elif condition; then
    # ...
# potentially more elif's here
else
    # ...
fi

While Loop

The loop runs until the specified condition is false.

while pgrep polybar; do
    killall polybar
done

Until Loop

The loop runs until the specified condition is true. This is the equivalent of while ! condition; do : done.

sudo apt update
# Repeatedly attempt to install neovim.
until which nvim &>/dev/null; do
    sudo apt install -y neovim
done

For Loop

Number Range

for i in {1..10}; do
    echo $i
done

Alternatively:

for i in $(seq 1 10); do
    echo $i
done

Iterating Over Array Elements

# Iterate over elements of `arr` directly
for elem in "${arr[@]}"; do
    echo $elem
done
 
# Iterate over indices of `arr`
for idx in "${!arr[@]}"; do
    echo ${arr[$idx]}
done

For more information, see Arrays.

Case (Switch) Statement

A case statement consist of an expression and different patterns, with each corresponding to one branch of code. The patterns are checked sequentially. If a certain pattern matches the expression, the flow of execution jumps to that branch. At the end of a branch, use ;; to jump out of the case statement entirely, or use ;& to execute the next branch without checking the pattern (since Bash 4.0).

case $test_expr in
pattern_a)
    echo "Pattern A"
    ;;
pattern_b)
    echo -n "Pattern B or "
    ;&
pattern_c)
    echo "Pattern C"
    ;;
pattern_d | pattern_e)
    echo "Pattern D or E"
    ;;
*)
    echo "Default case (matches all expressions)"
esac

Arithmetic

Basic Arithmetic

All bash literals are strings, but arithmetic operations are possible inside the $(()) operator. All basic arithmetic (+ - * / ** ( )) is possible within the double parentheses operator.

Converting Bases

Any base to decimal:

echo $((base#number))

Random Number

echo $RANDOM

Parameter Expansion

to-do

In progress

This section is incomplete.

Testing and Debugging

echo

Nothing’s better than just echoing stuff.

Stricter Options

set -euo pipefail
  • -e: fails the script on non-zero exit code
  • -u: substitution of unset variable will fail the script
  • -o pipefail: errors in a pipe will fail the script

Bash Built-in Logging

bash -x script.sh
  • Logs are prepended with +.
  • Logs variable assignment, commands executed, and the same for subshells (prepends additional + with each nested subshell).

No-Op

:

The colon is a built-in no-op in Bash.