clubmate.fi

A good[ish] website

Web development blog, loads of UI and JavaScript topics

Easily understandable introduction to shell scripting with bash

Filed under: Server— Tagged with: bash, shell

This post outlines some basic Bash programming concepts, such as: passing in params to a script, what is the shebang, how to define variables and functions, how to set immutable and scoped variables, how to handle conditional logic etc.

Bash isn’t necessarily a very trendy language, it’s a bit like a Lada in that sense, uncool but something that is there for you always.

Passing params to a bash script

Bash scripts are files that you call from the command line.

Say, if we would call a create_database.sh script like so:

$ create_database.sh db_name bob 5weetp45w0rd

In the file we can then access the script parameters with numbers $0.... 0 being the script name, ofter referred as prog_name:

#!/bin/bash

echo $0 # create_database.sh
echo $1 # db_name
echo $2 # bob
echo $3 # 5weetp45w0rd

The shebang

This #!/bin/bash in the beginning of the file. The characters hash and exclamation mark #! signals the beginning of the interpreter directive. It should be the very first line of the file.

The shebang is followed by the interpreter /bin/bash, an absolute path to a binary. The shebang can also look different i.e.: #!/usr/bin/env bash, depending what bash version you’re running (for example bash 4) or what system you’re on.

Variables in bash

Bash supports variable like any other language.

Setting variables

some_string="foo"

Global variables are usually written in all caps, it’s just a naming conventions, though. Generally it’s good to keep globals to minimum:

SOME_GLOBAL="foo"

Variables can always be assigned to a new value, sometimes unintentionally, that can prevented by making it readonly:

readonly SOME_GLOBAL="foo"

If a variable is defined inside a function, and is not needed outside the function, it can be declared as local:

hello_function() {
  local greeting="Hello world!"
}

That $greeting variable stays inside the function since it’s defined as local, and is only needed in the function. It’s nice that the variable can be scoped to the function, because: globals make programming cryptic, as they say.

Using variables

Access bash variables by prefixing the variable name with a dollar symbol:

string="Cheesy peas!"
echo $string

If another character is "touching" the end of the variable, it needs to be interpolated:

file_name=photo
echo path/to/file/some-${photo}.jpg

Bash functions

We already brushed function earlier, but here’s a simple example again:

say_hello() {
  echo "Hello!"
}

Call the function like so:

say_hello
# Prints "Hello!"

Function arguments

Arguments in a function are not defined like they are in, say, JavaScript. They can’t be named, they’re represented by a number, same way as script params are:

say_something() {
  echo $1 # Refer the first parameter as 1
  echo $2 # And the second as 2, and so on
}

# Usage:
say_something Hello You
# Hello
# You

If there’s more parameters, they’d be $4, $5... and so on. Since variables don’t have names, and if there’s a multitude of them, it might get cryptic. That number mess can be made more intelligible by reassigning the numbers to more human readable variables:

say_something() {
  # Vars
  local greeting=$1 # Note the local
  local user=$2

  # That looks pretty clear
  echo $greeting $user
}

# Use it:
say_something Hello You
# Hello You

The local keyword makes the variables accessible only in that function. A kind of a precautionary defensive best practice, you know.

Spaces in parameter names, use quotes:

say_something "Good day" "Mr. Slartibartfast"
# Outputs: Good day Mr. Slartibartfast

Here’s another, quite pointless example:

change_owner() {
  local filename=$1
  local user=$2
  local group=$3

  chown $user:$group $filename
}

Access all parameters with $@:

say_something() {
  echo $@
}

# Use it:
say_something Hello You

# Outputs:
Hello You

$# means the number of arguments passed to the function:

say_something() {
  # Vars
  local expected_args=2
  local greeting=$1
  local user=$2

  if [ $# -ne $expected_args ]; then
    echo "Usage: greeting user"
    exit
  else
    echo $greeting $user
  fi
}

Returning values from functions

You could echo out something in a function:

helloer() {
  echo "Hello!"
}

But many times you just want to return something rather. Bash doesn’t return values from functions like most other programming languages do. But, you can assign a value to a global variable and then use that variable:

some_fn() {
  output="something"
}

# Then use it like any other variable
echo "$output is a word."
# something is a word

Read more on this subject on this great article.

If else logic

Let’s use these three conditions as examples (scroll down for more condition examples):

[ -z $var ]
Checks if variable is empty, this will be our example condition.
[ "$var" = true ]
Checks if var is set to true.
[ -d $var ]
Checks if directory.
if [ -z $var ]
then
    echo "Something"
fi

Can also be written like this:

if [ -z $var ]; then
    echo "Something"
fi

Else if statement

The "unless" statement can be done with the elif keyword:

if [ -z $var ]
then
    echo "Something"
elif [ "$var" = true ]
    echo "Something else"
else
    echo "None matched"
fi

AND &&, and OR ||

Using 'AND' && and 'OR' ||. In this we need use the double bracket syntax:

if [[ -z $var ]] || [[ -d $dir ]]
then
    echo "Something"
fi

That checks if the variable $var has a value OR variable $dir is directory. Same thing for AND logic:

if [[ -z $var ]] && [[ -d $dir ]]
then
  echo "Something"
fi

Make it intelligible

Stuff like this: if [[ -z $var ]] && [[ ! -d $dir ]] might look a bit thick. Assigning stuff to functions can help make things more readable (or just learn them LOL).

A lot of things can be functions

Let’s take the above example, it has two conditions:

  1. Check if variable is empty
  2. Check if is directory

Let’s create two aptly named functions:

is_empty() {
  local var=$1
  [[ -z $var ]]
}

is_dir() {
  local dir=$1
  [[ -d $dir ]]
}

Use them like so:

if is_empty $var && is_dir $dir
then
    echo "Something"
fi

We went from jargon to English with very little effort.

You can bake any conditional command into a function like that, scroll down to see all the if expressions in Bash.

Using colors and text styles

Bash uses Ansi colors, the color codes look like this:

No color     0m
Black        0;30     Dark Gray     1;30
Blue         0;34     Light Blue    1;34
Green        0;32     Light Green   1;32
Cyan         0;36     Light Cyan    1;36
Red          0;31     Light Red     1;31
Purple       0;35     Light Purple  1;35
Brown/Orange 0;33     Yellow        1;33
Light Gray   0;37     White         1;37

Then you’d use them like this:

echo -e 'e[0;32m'Green text
# It seems to be working without the leading zero also
echo -e 'e[32m'Green text

After the green value '\e[32m', all text will be green, regardless of new lines etc. Better stop the green with no color:

echo -e 'e[0;32m'Green text'33[0m'

These won’t work in OS X bash, it just outputs the color codes as text, they need to be escaped with \033 and not with \e, like so:

echo -e '33[0;32m'Green text'33[0m'

That should work in Linux systems also (as far as I know, drop a comment if you have more knowledge on this).

To make it more humane, the colors can be turned into functions:

green() {
  echo -e '33[32m'$1'33[m'
}

red() {
  echo -e '33[31m'$1'33[m'
}

# Usage
red "This is some red text"

The colors and text styling can be set to variables. Here’s a whole set of them:

RCol='33[0m' # Text Reset

# Regular        Bold              Underline         High Intensity    BoldHigh Intens    Background        High Intensity BGs
Bla='33[0;30m';  BBla='33[1;30m';  UBla='33[4;30m';  IBla='33[0;90m';  BIBla='33[1;90m';  On_Bla='33[40m';  On_IBla='33[0;100m';
Red='33[0;31m';  BRed='33[1;31m';  URed='33[4;31m';  IRed='33[0;91m';  BIRed='33[1;91m';  On_Red='33[41m';  On_IRed='33[0;101m';
Gre='33[0;32m';  BGre='33[1;32m';  UGre='33[4;32m';  IGre='33[0;92m';  BIGre='33[1;92m';  On_Gre='33[42m';  On_IGre='33[0;102m';
Yel='33[0;33m';  BYel='33[1;33m';  UYel='33[4;33m';  IYel='33[0;93m';  BIYel='33[1;93m';  On_Yel='33[43m';  On_IYel='33[0;103m';
Blu='33[0;34m';  BBlu='33[1;34m';  UBlu='33[4;34m';  IBlu='33[0;94m';  BIBlu='33[1;94m';  On_Blu='33[44m';  On_IBlu='33[0;104m';
Pur='33[0;35m';  BPur='33[1;35m';  UPur='33[4;35m';  IPur='33[0;95m';  BIPur='33[1;95m';  On_Pur='33[45m';  On_IPur='33[0;105m';
Cya='33[0;36m';  BCya='33[1;36m';  UCya='33[4;36m';  ICya='33[0;96m';  BICya='33[1;96m';  On_Cya='33[46m';  On_ICya='33[0;106m';
Whi='33[0;37m';  BWhi='33[1;37m';  UWhi='33[4;37m';  IWhi='33[0;97m';  BIWhi='33[1;97m';  On_Whi='33[47m';  On_IWhi='33[0;107m';

Source

Possible usage:

error_message() {
  echo "$Red Something went wrong. $RCol"
}

All the if expressions in bash

And here’s a table of all the if expressions in Bash:

PrimaryMeaning
[ -a FILE ]True if FILE exists.
[ -b FILE ]True if FILE exists and is a block-special file.
[ -c FILE ]True if FILE exists and is a character-special file.
[ -d FILE ]True if FILE exists and is a directory.
[ -e FILE ]True if FILE exists.
[ -f FILE ]True if FILE exists and is a regular file.
[ -g FILE ]True if FILE exists and its SGID bit is set.
[ -h FILE ]True if FILE exists and is a symbolic link.
[ -k FILE ]True if FILE exists and its sticky bit is set.
[ -p FILE ]True if FILE exists and is a named pipe (FIFO).
[ -r FILE ]True if FILE exists and is readable.
[ -s FILE ]True if FILE exists and has a size greater than zero.
[ -t FD ]True if file descriptor FD is open and refers to a terminal.
[ -u FILE ]True if FILE exists and its SUID (set user ID) bit is set.
[ -w FILE ]True if FILE exists and is writable.
[ -x FILE ]True if FILE exists and is executable.
[ -O FILE ]True if FILE exists and is owned by the effective user ID.
[ -G FILE ]True if FILE exists and is owned by the effective group ID.
[ -L FILE ]True if FILE exists and is a symbolic link.
[ -N FILE ]True if FILE exists and has been modified since it was last read.
[ -S FILE ]True if FILE exists and is a socket.
[ FILE1 -nt FILE2 ]True if FILE1 has been changed more recently than FILE2, or if FILE1 exists and FILE2 does not.
[ FILE1 -ot FILE2 ]True if FILE1 is older than FILE2, or is FILE2 exists and FILE1 does not.
[ FILE1 -ef FILE2 ]True if FILE1 and FILE2 refer to the same device and inode numbers.
[ -o OPTIONNAME ]True if shell option "OPTIONNAME" is enabled.
[ -z STRING ]True of the length if "STRING" is zero.
[ -n STRING ] or [ STRING ]True if the length of "STRING" is non-zero.
[ STRING1 == STRING2 ]True if the strings are equal. "=" may be used instead of "==" for strict POSIX compliance.
[ STRING1 != STRING2 ]True if the strings are not equal.
[ STRING1 < STRING2 ]True if "STRING1" sorts before "STRING2" lexicographically in the current locale.
[ STRING1 > STRING2 ]True if "STRING1" sorts after "STRING2" lexicographically in the current locale.
[ ARG1 OP ARG2 ]"OP" is one of -eq, -ne, -lt, -le, -gt or -ge. These arithmetic binary operators "ARG2", respectively. "ARG1" and"ARG2"are integers.

Table lifted from here.

Conclusions

This just the very basics.

Comments would go here, but the commenting system isn’t ready yet, sorry.

  • © 2022 Antti Hiljá
  • About
  • All rights reserved yadda yadda.
  • I can put just about anything here, no one reads the footer anyways.
  • I love u!