Small Peek into Bash

Linux banner
Reading Time: 8 minutes

What is Bash ? or what’s that Black screen that normally appears in Linux as Terminal.
Thats what is Bash and we are going to have a peek of it in this Blog.

Bash is a Unix shell written by Brian Fox for the GNU Project as a free software replacement for the Bourne shell. It was released in 1989 and has been distributed as the Linux and macOS default shell for a long time.

Shells and modes

The user bash shell can work in interactive and non-interactive login shells.

  1. interactive login shell: You log into a remote computer via, for example ssh. Alternatively, you drop to a tty on your local machine (Ctrl+Alt+F1) and log in there.
  2. interactive non-login shell: Open a new terminal.
  3. non-interactive non-login shell: Run a script. All scripts run in their own sub shell and this shell is not interactive.
  4. non-interactive login shell: This is extremely rare, and you’re unlikely to encounter it. One way of launching one is echo command | ssh server. When ssh is launched without a command (so ssh instead of ssh command which will run command on the remote shell) it starts a login shell. If the stdin of the ssh is not a tty, it starts a non-interactive shell. This is why echo command | ssh server will launch a non-interactive login shell. You can also start one with bash -l -c command.

Comments

Scripts may contain comments. Comments are special statements ignored by the shell interpreter. They begin with a # symbol and continue on to the end of the line.

Given Examples will clear it for you:

#!/bin/bash
# This script will print your username.
whoami

Tip: Use of comments for explaining the purpose of script is always a plus point.

Variables

Variables creating in Bash is as simple as most programming language.

Bash knows no data types. Variables can contain only numbers or a string of one or more characters. There are three kinds of variables you can create: local variables, environment variables and variables as positional arguments.

Local variables

Local variables are variables that exist only within a single script. They are inaccessible to other programs and scripts.

A local variable can be declared using = sign (as a rule, there should not be any spaces between a variable’s name, = and its value) and its value can be retrieved using the $ sign. eg:

username="knoldus"  # declare variable
echo $username          # display value
unset username          # delete variable

We can also declare a variable local to a single function using the local keyword.

local local_var="I'm a local value"

Environment variables

Environment variables are variables accessible to any program or script running in current shell session. They are created just like local variables, but using the keyword export instead.

export GLOBAL_VAR="I'm a global variable"

There are a lot of global variables in bash. Here is a quick lookup table with the most practical ones:

VariableDescription
$HOMEThe current user’s home directory.
$PATHA colon-separated list of directories in which the shell looks for commands.
$PWDThe current working directory.
$RANDOMRandom integer between 0 and 32767.

Arrays

Like in other programming languages, an array in bash is a variable that allows you to refer to multiple values.

When dealing with arrays, we should be aware of the special environment variable IFS. IFS, or Input Field Separator, is the character that separates elements in an array. The default value is an empty space IFS=' '.

Array declaration

In bash you create an array by simply assigning a value to an index in the array variable:

fruits[0]=Apple
fruits[1]=Pear
fruits[2]=Plum

fruits=(Apple Pear Plum)

Individual array elements are expanded similar to other variables:

echo ${fruits[1]} # Pear

The entire array can be expanded by using * or @ in place of the numeric index:

echo ${fruits[*]} # Apple Pear Plum
echo ${fruits[@]} # Apple Pear Plum

Array slice

We can extract a slice of array using the slice operators:

echo ${fruits[@]:0:2} # Apple Desert fig

In the example above, ${fruits[@]} expands to the entire contents of the array, and :0:2 extracts the slice of length 2, which starts at index 0.

Adding elements into an array

Adding elements into an array is quite simple too. We can use them like this:

fruits=(Orange "${fruits[@]}" Banana Cherry)
echo ${fruits[@]} # Orange Apple Desert fig Plum Banana Cherry

The example above, ${fruits[@]} expands to the entire contents of the array and substitutes it into the compound assignment, then assigns the new value into the fruits array mutating its original value.

Deleting elements from an array

To delete an element from an array, use the unset command:

unset fruits[0]
echo ${fruits[@]} # Apple Desert fig Plum Banana Cherry

Streams, pipes and lists

Bash has powerful tools for working with other programs and their outputs.

Pipes give us opportunity to create conveyors and control the execution of commands.

It is paramount we understand how to use this powerful and sophisticated tool.

Streams

Bash receives input and sends output as sequences or streams of characters.

There are three descriptors:

CodeDescriptorDescription
0stdinThe standard input.
1stdoutThe standard output.
2stderrThe errors output.

Redirection makes it possible to control where the output of a command goes to, and where the input of a command comes from. For redirecting streams these operators are used:

OperatorDescription
>Redirecting output
&>Redirecting output and error output
&>>Appending redirected output and error output
<Redirecting input
<<Here documents syntax
<<<Here strings

Redirection examples :

# output of ls will be written to list.txt
ls -l > list.txt

# append output to list.txt
ls -a >> list.txt

# all errors will be written to errors.txt
grep da * 2> errors.txt

# read from errors.txt
less < errors.txt

Pipes

In the example below, command1 sends its output to command2, which then passes it on to the input of command3:

command1 | command2 | command3

Constructions like this are called pipelines.

In practice, this can be used to process data through several programs. For example, here the output of ls -l is sent to the grep program, which prints only files with a .md extension, and this output is finally sent to the less program:

ls -l | grep .md$ | less

If you want your pipelines to be considered a failure if any of the commands in the pipeline fail, you should set the pipefail option with:

set -o pipefail

Lists of commands

A list of commands is a sequence of one or more pipelines separated by ;, &, && or || operator.

If a command is terminated by the control operator &, the shell executes the command asynchronously in a subshell.

Commands separated by a ; are executed sequentially: one after another. The shell waits for the finish of each command.

# command2 will be executed after command1
command1 ; command2

# which is the same as
command1
command2

Lists separated by && and || are called AND and OR lists, respectively.

The AND-list looks like this:

# command2 will be executed if, and only if, command1 finishes successfully (returns 0 exit status)
command1 && command2

The OR-list has the form:

# command2 will be executed if, and only if, command1 finishes unsuccessfully (returns code of error)
command1 || command2


Working with the file system:

PrimaryMeaning
[ -e FILE ]True if FILE exists.
[ -f FILE ]True if FILE exists and is a regular file.
[ -d FILE ]True if FILE exists and is a directory.
[ -s FILE ]True if FILE exists and not empty (size more than 0).
[ -r FILE ]True if FILE exists and is readable.
[ -w FILE ]True if FILE exists and is writable.
[ -x FILE ]True if FILE exists and is executable.
[ -L FILE ]True if FILE exists and is symbolic link.
[ FILE1 -nt FILE2 ]FILE1 is newer than FILE2.
[ FILE1 -ot FILE2 ]FILE1 is older than FILE2.

Working with strings:

PrimaryMeaning
[ -z STR ]STR is empty (the length is zero).
[ -n STR ]STR is not empty (the length is non-zero).
[ STR1 == STR2 ]STR1 and STR2 are equal.
[ STR1 != STR2 ]STR1 and STR2 are not equal.

Arithmetic binary operators:

PrimaryMeaning
[ ARG1 -eq ARG2 ]ARG1 is equal to ARG2.
[ ARG1 -ne ARG2 ]ARG1 is not equal to ARG2.
[ ARG1 -lt ARG2 ]ARG1 is less than ARG2.
[ ARG1 -le ARG2 ]ARG1 is less than or equal to ARG2.
[ ARG1 -gt ARG2 ]ARG1 is greater than ARG2.
[ ARG1 -ge ARG2 ]ARG1 is greater than or equal to ARG2.

Conditions may be combined using these combining expressions:

OperationEffect
[ ! EXPR ]True if EXPR is false.
[ (EXPR) ]Returns the value of EXPR.
[ EXPR1 -a EXPR2 ]Logical AND. True if EXPR1 and EXPR2 are true.
[ EXPR1 -o EXPR2 ]Logical OR. True if EXPR1 or EXPR2 are true.

Sure, there are more useful primaries and you can easily find them in the Bash man pages.

Using an if statement

if statements work the same as in other programming languages. If the expression within the braces is true, the code between then and fi is executed. fi indicates the end of the conditionally executed code.

# Single-line
if [[ 1 -eq 1 ]]; then echo "true"; fi

# Multi-line
if [[ 1 -eq 1 ]]; then
  echo "true"
fi

Likewise, we could use an if..else statement such as:

# Single-line
if [[ 2 -ne 1 ]]; then echo "true"; else echo "false"; fi

# Multi-line
if [[ 2 -ne 1 ]]; then
  echo "true"
else
  echo "false"
fi

Sometimes if..else statements are not enough to do what we want to do. In this case we shouldn’t forget about the existence of if..elif..else statements, which always come in handy.

Look at the example below:

if [[ `uname` == "Adam" ]]; then
  echo "Do not eat an apple!"
elif [[ `uname` == "Eva" ]]; then
  echo "Do not take an apple!"
else
  echo "Apples are delicious!"
fi

Using a case statement

If you are confronted with a couple of different possible actions to take, then using a case statement may be more useful than nested if statements. For more complex conditions use case like below:

case "$extension" in
  "jpg"|"jpeg")
    echo "It's image with jpeg extension."
  ;;
  "png")
    echo "It's image with png extension."
  ;;
  "gif")
    echo "Oh, it's a giphy!"
  ;;
  *)
    echo "Woops! It's not image!"
  ;;
esac

Each case is an expression matching a pattern. The | sign is used for separating multiple patterns, and the ) operator terminates a pattern list. The commands for the first match are executed. * is the pattern for anything else that doesn’t match the defined patterns. Each block of commands should be divided with the ;; operator.

Loops

There are four types of loops in Bash: for, while, until and select.

for loop

The for is very similar to its sibling in C. It looks like this:

for arg in elem1 elem2 ... elemN
do
  # statements
done

During each pass through the loop, arg takes on the value from elem1 to elemN. For example, if we need to move all .bash files into the script folder and then give them execute permissions, our script would look like this:

#!/bin/bash

for FILE in $HOME/*.bash; do
  mv "$FILE" "${HOME}/scripts"
  chmod +x "${HOME}/scripts/${FILE}"
done

while loop

The while loop tests a condition and loops over a sequence of commands so long as that condition is true. A condition is nothing more than a primary as used in if..then conditions. So a while loop looks like this:

while [[ condition ]]
do
  # statements
done

Just like in the case of the for loop, if we want to write do and condition in the same line, then we must use a semicolon before do.

A working example might look like this:

#!/bin/bash

# Squares of numbers from 0 through 9
x=0
while [[ $x -lt 10 ]]; do # value of x is less than 10
  echo $(( x * x ))
  x=$(( x + 1 )) # increase x
done

until loop

The until loop is the exact opposite of the while loop. Like a while it checks a test condition, but it keeps looping as long as this condition is false:

until [[ condition ]]; do
  #statements
done

select loop

The select loop helps us to organise a user menu. It has almost the same syntax as the for loop:

select answer in elem1 elem2 ... elemN
do
  # statements
done

The select prints all elem1..elemN on the screen with their sequence numbers, after that it prompts the user. Usually it looks like $? (PS3 variable). The answer will be saved in answer. If answer is the number between 1..N, then statements will execute and select will go to the next iteration — that’s because we should use the break statement.

A working example might look like this:

#!/bin/bash

PS3="Choose the package manager: "
select ITEM in npm gem pip
do
  echo -n "Enter the package name: " && read PACKAGE
  case $ITEM in
    npm)   npm   install $PACKAGE ;;
    gem)   gem   install $PACKAGE ;;
    pip)   pip   install $PACKAGE ;;
  esac
  break # avoid infinite loop
done

This example, asks the user what package manager would like to use.

If we run this, we will get:

$ ./my_script
1) npm
2) gem
3) pip
Choose the package manager: 2
Enter the package name: bash-handbook
<installing bash-handbook>

Functions

In scripts we have the ability to define and call functions.

Calling a function is the same as calling any other program, you just write the name and the function will be invoked.

We can declare our own function this way:

my_func () {
  # statements
}

my_func # call my_func

We must declare functions before we can invoke them.

Below is a function that takes a name and returns 0, indicating successful execution.

# function with params
greeting () {
  if [[ -n $1 ]]; then
    echo "Hello, $1!"
  else
    echo "Hello, unknown!"
  fi
  return 0
}

greeting Manas  # Hello, Manas!
greeting        # Hello, unknown!

For more info run the man page of bash.