JREAM

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:

VariableValue
$HOME/home/username/
$USERusername
$TERMxterm-256color
$PATHAll 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.

MethodDescriptionClosing Statement
ifOpening If statementfi
elseNested Else statementnone
else ifNested Else If statementnone
elifAlias for Else If statementnone

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.

StreamValueDescription
STDIN0Standard Input, the data coming from the user.
STDOUT1Standard Output, output stream going to to the user.
STDERR2Standard 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.

CommandRedirect to
1>STDOUT (default)
2>STDERR
2>&1STDERR 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 )).

TypeOperatorPurpose
Expression'!'False
Compound-aLogical and like &&
Compound-oLogical or like `
String-nString is not NULL (>0 length)
String-zString 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-eqInteger is equal
Integer-geInteger is greater than or equal
Integer-gtInteger is greater than
Integer-leInteger is less than or equal
Integer-ltInteger is less than
Integer-neInteger 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-ntFile is newer than
File-otFile is older than
File-dFile exists and is a directory
File-eFile exists
File-fFile exists and is a regular file
File-hFile exists and is a symbolic link (same as -L)
File-LFile exists and is a symbolic link (same as -h)
File-OFile exists and is owned by current user ID
File-sFile exists and size is greater than zero
File-rFile exists and is readable
File-wFile exists and is writable
File-xFile 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.