Bash Scripting Basics
All you need to do is type echo $SHELL
to see which shell you're using. You can also see which user (or service) uses which shell with cat /etc/passwd | grep username
. If you want to change your default shell here is how:
# See a list of shells available
cat /etc/shells
# Set a default shell for a user
sudo usermod --shell /bin/bash username
# Or, with CHangeSHell
sudo chsh -s /bin/bash username
Table of Contents
Global Variables
You have global variables which you can list with env
. Some examples you'll commonly use are:
Variable | Value |
---|---|
$HOME | /home/username/ |
$USER | username |
$TERM | xterm-256color |
$PATH | All accessible paths for current user |
The purpose of knowing a few of these is to generalize your bash scripts as much as possible like you would in any other programming language.
Practicing Bash scripting
To practice bash scripting simply make a new file called learn.sh
.
#!/bin/bash
echo 'It Works'
Now you can practice your code in this file. You can run this file one of two ways; With bash <file>
or chmod u+x <file>
which will allow your user to execute the file.
bash learn.sh
./learn.sh
Lastly, you can type in your terminal to test things out. If you want to do a multiline command you escape \
before the next line.
echo 'hello' \
echo 'world'
If Else Statements
There are several ways to do if/else statements so I'll show you a variety. You should choose a method and stick to that way of doing it throughout your scripts for consistency.
Method | Description | Closing Statement |
---|---|---|
if | Opening If statement | fi |
else | Nested Else statement | none |
else if | Nested Else If statement | none |
elif | Alias for Else If statement | none |
The purest form looks like this on multiple lines.
if true
then
echo 'true'
else if 'nothing'
then
echo 'unknown'
else
echo 'false'
fi
We can format this to make it more readable with then
on the same line by adding a semi-colin ;
if true; then
echo 'true'
else if 'nothing'; then
echo 'nothing'
else
echo 'false'
fi
We can also use a ternary operator, but grouping multiple items can look sloppy. The &&
is for AND
and ||
is for OR
.
true && echo 'true' || echo 'false'
true && (echo 'true' && echo 'true') || echo 'false'
Using the ternary can go both ways. We can also use the ||
operator which is useful for assigning variables.
# if (true) then echo 'true'
true && echo 'true' # true
# if (true) .. else echo 'false'
true || echo 'false' # empty
# Assign a variable, sets to 50 because 50 is a number, so it's true.
a = 50 || 100
echo $a
# Run a command
In order to check values based on conditions and flags we need to put them in braces []
, this is also called the test
command. With the braces it's important that you put a space between the brackets otherwise bash thinks its a command. You can type these in the terminal as well as include them in a bash script.
# Wrong, it thinks "[10" is a command
[10 == 10] && echo 'true'
# Correct
[ 10 == 10 ] && echo 'true'
test 10 == 10 && echo 'true'
test 10 -eq 10 && echo 'true'
In a file with more than one result on being true it looks nicer to us the if block rather than the ternary.
if [ 10 == 10 ]; then
echo 'true'
echo '10 is zGreat News!'
fi
Double brackets make your code less portable, that is, it's primarily a bash shell thing and not part of POSIX standards. It's up to you if you want to use [[ brackets ]]
, I do. This allows &&
or ||
()
<
operators to work .
a=50
# This fails due to the &&
[ $a > 5 && $a < 100 ] && echo true;
# This allows the && operator to work
[[ $a > 5 && $a < 100 ]] && echo true;
You'll also want to keep an eye on your quotes because they make a difference.
name='jesse'
# False, because the string must be double quoted
[ name == 'jesse' ] && echo 'true'
# True, the double quotes compare the string correctly
[ name == "jesse" ] && echo 'true'
# Oddly, both of these work from this ENV variable.
[ $PWD == '/home/jesse' ] && echo 'true'
[ $PWD == "/home/jesse" ] && echo 'true'
Streams: Input/Output/Error
There are three standard streams Linux uses which are standardized by POSIX (a place that sets standards for computing). These are useful to know because you will often see >
and 2>
and 2>&1
in your scripts and wonder what they do.
Stream | Value | Description |
---|---|---|
STDIN | 0 | Standard Input, the data coming from the user. |
STDOUT | 1 | Standard Output, output stream going to to the user. |
STDERR | 2 | Standard Error, output stream going to to the user. |
Knowing we have these streams we can use them to redirect the results based on their value. When you type a command in the terminal, the output is returned to the terminal. Sometimes it comes from STDOUT
and sometimes from STDERR
.
To demonstrate, lets first run a command to Here we run two commands with a |
left to right into STDOUT
.
# Redirect the (ls) directory listing to (cat) concatenate,
# This makes the list into a single column.
ls | cat
In order to redirect we use the >
operator. Here we will redirect the STDOUT
to a file.
# Results into text file
ls > list.txt
cat list.txt
# Write a file
echo "Hello World" > hello.txt
cat hello.txt
We can get the output of STDERR
by typing a bad command. Even though an error message shows in the terminal the same place as STDOUT
it is still coming from the STDERR
stream.
# Run a fake command and capture the STDOUT
jello > exists.txt
cat exists
Above, we see there was output from the bad command but it was not written to the file because it came from the STDERR
stream. This means we can use the redirect operator >
with a stream value to be specific. This is useful when we want to capture command output, and log or silence errors.
Command | Redirect to |
---|---|
1> | STDOUT (default) |
2> | STDERR |
2>&1 | STDERR into STDOUT |
# Run a fake command and capture the STDERR
jello 2> exists.txt
cat exists.txt
Now we captured the STDERR
message properly. You may run into situations where you have are running a script that has STDOUT
and STDERR
coming from different areas. In that case we can handle that as well.
Make a new file which will output to both streams. Create a file touch std.sh && chmod u+x std.sh
, and add the contents to it below.
#!/bin/bash
# filename: std.sh (chmod u+x std.sh)
echo "This is going to the STDOUT stream"
jello
Now we can redirect the STDOUT
to the STDERR
stream. First run ./std.sh
and we get the following error.
This is going to the STDOUT stream
./std.sh: line 5: jello: command not found
Let's redirect that into separate files.
# 1> Redirects to STDOUT
# 2> Redirects to STDERR
./std.sh 1> stdout.txt 2> stderr.txt
cat stdout.txt
cat stderr.txt
To capture both STREAMS in the same file we first redirect to a file and then to the STREAMS by using &>
we can redirect a STREAM. To say 2>&1
is to also redirect the STDERR to the STDOUT.
./std.sh > results.txt 2>&1
cat results.txt
Finally, if we want to silence errors we send them off /dev/null
. A use case is when checking if a command exists, do not output the error but go through the conditional statement instead.
# See our error message
jello
# Try to redirect it, oops STDOUT is the wrong stream.
jello > /dev/null
# Redirect STDERR
jello 2> /dev/null
# Redirect both STDERR and STDOUT if needed
jello > /dev/null 2>&1
Combining Conditionals and Streams
Now we can make our own bash files and make sure we aren't capturing a lot of errors. To see all the features your can use within the test
[]
bracket function type man test
. The reason these are often using flags -gt
rather than certain >
<
>=
symbols otherwise you must wrap them in either [[ double ]]
brackets or (( double parentheses ))
.
Type | Operator | Purpose |
---|---|---|
Expression | '!' | False |
Compound | -a | Logical and like && |
Compound | -o | Logical or like ` |
String | -n | String is not NULL (>0 length) |
String | -z | String is NULL (0 length) |
String | = | Strings are equal |
String | == | Strings are equal to (Different with double brackets) |
[[ 'zebra' == z* ]] # True (pattern matching) | ||
[[ 'zebra' == "z*" ]] # False (literal matching) | ||
[ 'zebra' == z ] # False (file globbing/word splitting happening) | ||
[ "zebra" == "z*" ] # True (literal matching) | ||
String | != | Strings are not equal |
Integer | -eq | Integer is equal |
Integer | -ge | Integer is greater than or equal |
Integer | -gt | Integer is greater than |
Integer | -le | Integer is less than or equal |
Integer | -lt | Integer is less than |
Integer | -ne | Integer is not equal to |
Integer | > | Integer is greater than ((Double Parentheses)) |
Integer | >= | Integer is greater than or equal to ((Double Parentheses)) |
Integer | < | Integer is less than ((Double Parentheses)) |
Integer | <= | Integer is less than or equal to ((Double Parentheses)) |
File | -nt | File is newer than |
File | -ot | File is older than |
File | -d | File exists and is a directory |
File | -e | File exists |
File | -f | File exists and is a regular file |
File | -h | File exists and is a symbolic link (same as -L) |
File | -L | File exists and is a symbolic link (same as -h) |
File | -O | File exists and is owned by current user ID |
File | -s | File exists and size is greater than zero |
File | -r | File exists and is readable |
File | -w | File exists and is writable |
File | -x | File exists and is executable |
I'll use a few different formats you can try.
# Check if a directory exists
[ -d $HOME ] && echo "$HOME exists"
if [ -d $HOME ]; then echo "$HOME exists"; fi
# Check with a False Expression False
[ ! -d $HOME ] && echo "does not exist" || echo "exists"
if [ ! -d $HOME ]; then echo "does not exist"; else echo "exists"; fi
# Check if a directory exists combining a variable and string
if [ -d "$HOME/.nvm" ] && echo 1
# Check if a file exists combining a variable and string
if [ -f "$HOME/.bashrc" ]; then echo 1; fi
# Use the greater than symbol ">" instead of -gt
(( 2 > 1)) && echo 'true'
[ 2 -gt 1 ] && echo 'true'
# Test if a command exists
[ -x "$(command -v jello)" ] && echo 'exists' || echo 'does not exist'
if [ -x "$(command -v vtop)" &> /dev/null ]; then echo 'exists'; fi
if (( $+commands[direnv] )) > /dev/null 2>&1; then echo 'exists'; fi
# Test if a function has a definition
hash nmap 2>/dev/null && echo 'exists'
if hash nmap 2>/dev/null; then echo 'exists'; fi
# Execute a bash command with eval
eval "$(source ~/.bashrc)"
Using Function Arguments
You can create functions which will behave the same as aliases. Your functions can do more. For example, if you wanted to pass data into a function such as: hello new person
, you can grab the first and second arguments with $1
and $2
respectively. You would likely want to check with -z
so it's not empty.
function hello(){
if [ -z "$1" ];then
echo "[!] You must pass an argument
fi
if [ -z "$2" ];then
echo "[!] You must pass a second argument
fi
}
Here's an example of using a functions argument to make use of extracting any type of compressed file.
extract() {
if [ -f "$1" ]; then
FILE=$1
case $FILE in
*.7z) 7z x $FILE ;;
*.bz2) bunzip2 $FILE ;;
*.gz) gunzip $FILE ;;
*.rar) unrar x $FILE ;;
*.tar.bz2) tar xjf $FILE ;;
*.tar.gz) tar xzf $FILE ;;
*.tar) tar xf $FILE ;;
*.tgz) tar xzf $FILE ;;
*.zip) unzip $FILE ;;
*)
echo "[!] '$FILE' cannot be extracted."
echo "Formats tried: 7z, bz2, gz, rar, tar, tar.bz2, tar.gz, tgz, zip."
;;
esac
else
echo "[!] Cannot extract $FILE"
fi
}
Variables
There are a few rules to variables in Bash to cover. Otherwise you'll not have a clue why something fails because you get no warning.
Variables must not have a space, and they do not start with a $
symbol - that's only when you call it.
name='Jesse'
middle="Jesse"
age=37
Notice the string can be single or double quotes. The difference is that double quotes will parse variables within them. You can surround them in curly braces to put them beside other characters as well.
name='Jesse'
echo "Hello $name"
echo "Hello ${name}."
When you export
a variable it becomes global to your user. That is, as long as your shell session is open unless you saved it in your ~/.bashrc
file. The reason it would always preserve is because .bashrc
is always loading for every terminal session among other files.
Try creating a file called example.sh
and we will echo out two variables and hope they are known in our terminal.
#!/bin/bash
echo "Foo: $FOO"
echo "Bar: $BAR"
Now let's assign the variables (in the terminal) and see what happens with using export
.
BAR='bar'
export FOO='foo'
bash example.sh
You will only see the example.sh
file output the $FOO
variable because it was exported. What's happening is that when you run the script it running a child shell that does not inherit every variable - that would be crazy. So you use export
to pass it along and to pass a variable form file to file.
You can also export functions using the -f
flag before the export.
#!/bin/bash
function name() {
if [ -z $1 ]; then
echo "Pass a name for your argument"
return
fi
echo "Hello $1"
}
export -f name
To view all exported variables.
export -p
I hope this was a good introduction to some bash scripting, have fun and don't be afraid to use the man
page for help in your terminal.