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"
See also 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”).
Name | fd | Purpose |
---|---|---|
stdin | 0 | Programs receive user input from stdin. |
stdout | 1 | Programs write output to stdout. |
stderr | 2 | Programs 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
Operator | Expression is true when… |
---|---|
! EXPR | EXPR is false. |
-n STRING | STRING is not empty (non-zero-sized) |
-z STRING | STRING is empty (zero-sized) |
STRING1 != STRING2 | STRING1 is not equal to STRING2 |
STRING1 = STRING2 | STRING1 is equal to STRING2 |
INTEGER1 -eq INTEGER2 | INTEGER1 is equal to INTEGER2 |
INTEGER1 -ne INTEGER2 | INTEGER1 is not equal to INTEGER2 |
INTEGER1 -gt INTEGER2 | INTEGER1 is greater than INTEGER2 |
INTEGER1 -lt INTEGER2 | INTEGER1 is less than INTEGER2 |
INTEGER1 -ge INTEGER2 | INTEGER1 is greater than or equal to INTEGER 2 |
INTEGER1 -le INTEGER2 | INTEGER1 is less than or equal to INTEGER 2 |
-e PATH | PATH exists |
-f PATH | PATH exists and is a flie |
-d PATH | PATH exists and is a directory |
-r PATH | PATH exists and is readable |
-s PATH | PATH exists and is nonempty |
-w PATH | PATH exists and is writeable |
-x PATH | PATH exists and is executable |
EXPR1 -o EXPR2 | at least one of EXPR1 or EXPR2 is true |
EXPR1 -a EXPR2 | both EXPR1 and EXPR2 are true |
Remember to quote your strings
Quote string operands when comparing using
[ ]
. Single square bracket pairs refer to thetest
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
- See Boolean Expression.
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
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.