Shell Scripting
March 18, 2020

Shell scripts | Functions and flow control

Sometimes we want to store more than a single value in a variable. And sometimes decisions have to be made for a hundred times. Let's jump into creating arrays, flow control with loops and how to organize our commands with functions, so we can store them into scripts.

This is a continuation from this episode. In this second part we'll take a look at flow control using for and while loops, how arrays work inside shell scripting and the use of functions.

If we want to evaluate a value to perform an action, we can store the value in a variable and perform the desired action, but what about having to evaluate more than one value at a time?

Let's say we have a directory with a lot of files, and we need to get rid of some of them, rename the backups and print a file-size sorted list to a log. Having to perform each action individually will take too many time. Luckily there are ways to make it in an automated form.

Flow control: for loop

This way of flow control works iterating trough values in a list until the end is reached. for loops perform a set of commands for each item in the list.

A simple for loop structure looks like this:

for var in values; do
  #commands to execute $var
done

As an example, let's imagine we have a directory with a bunch of files, and we want to list only which of them are .png images.

myDirectory=/path/to/myDirectory/
images=*.png

for i in $myDirectory$images; do
   printf "%s\n" "$i"
done

— The shell also understands C-Style for loops, which have this structure:

for (var=1; var < n; var++); do
  #commands to execute $var
done

— We can perform some control inside a for loop using the continue and break commands.

  • continue tells the Shell to skip the current loop and jump into the next value in "values".
for var in values; do
  #command a 
  #command b
  if(condition to jump over c); then
    continue
  fi
  #command c
done
  • break tells the Shell to leave the for loop straight away.
for var in values; do
  #command a 
  #command b
  if(condition to break the loop); then
    command c
    break
  fi
  #command d
done

— As in other programming languages, we can perform nested for loops, this is, a loop within a loop. They are handy when we want to repeat more than one action several times. They can be independent or not.

for (i=1; i < n; i++); do
  #commands to execute $i (if any before entering the next loop)
  for (j=1; j < i; j++); do
    #commands to execute $i $j
  done
done

Flow control: while loop

As their name indicate, while an expression is true, the loop will run the inner lines of code.

A simple while loop structure looks like this:

while [ test ]; do
  #commands to execute
done

while loops evaluate the exit status to check if they have to stop or not. A while loop will run for as long as the exit status evaluated inside [[]] equals to zero.

num=0
while [[ "$num" -lt 5 ]]; do
   printf "%s\n" "$num"
   num=$((num + 1))
done
printf "%s\n" "We've reached the limit."

— The same way we are able to control for loops with continue and break, we can control while loops too.

num=0
while [[ "$num" -lt 5 ]]; do
   if [[ "$num" == 3 ]]; then
      printf "%s\n" "exited because num is equal to 3."
      break
   done
   num=$((num + 1))
done

while loops can be controlled by user input too taking the advantage of infinite loops.

Infinite loops are defined by adding : after the word while.

while :
do
  #commands inside infinite loop
done

To stop an infinite loop, press the CTRL+C key combination, or include a value to be understood as a loop end:

while :
do
  read -p "type EXIT to end program: " end
  printf "%s\n" "You typed $end"
  if [ $end == "EXIT" ]; then
    exit 0
  done
done

Arrays

Arrays are variables that have the ability to hold more than one value at a time. They have elements that behave like cells, and each of them stores data that can be accessed via an index.

Arrays in Shell scripting have some characteristics:

  • The values of an array are separated by spaces.
  • Arrays are limited to a single dimension.

— Arrays are declared the same way as a variable inside Shell scripting, however we can assign values in several ways.

  • List assignment:
numArray=(0 1 2 3)
strArray=('a' 'b' 'c' 'd')
  • Subscript assignment:
elemArray=([0]='first element' [1]='second element')
  • Index assignment:
strArray[0]='a'
strArray[1]='b'
  • Dynamic assignment:
imgArray=(*.png)
argsArray=("$@")

— Once we've created an array we can also modify it via adding more elements, replacing the entire array with new elements, merging various arrays together or deleting indexes.

  • Change index to initialize or update a specific element inside the array
strArray[9]='j'
  • Append elements to the array. We can add elements both to the end and the beginning of the array, or we can replace the entire array
# create an array
numArray=(0 1 2 3 4)

# add elements at the end of the array
numArray+=(5 6)

# add elements at the beginning of the array
numArray=(-2 -1 "${numArray[@]}"

# replace the entire array with new element list
numArray=("${numArray[@]}" 5 6)
  • Merge arrays together
arrayA=(0 1 2 3 4)
arrayB=(5 6 7 8 9)
arrayC=("${arrayA[@]}" "${arrayB[@]}")
  • Delete arrays and/or indexes using unset:

We can unset an entire array by just passing unset in it:

arrayA=('first' 'second' 'third')
printf "%s\n" "Before unset: ${arrayA[@]}"
unset arrayA
printf "%s\n" "After unset: ${arrayA[@]}"

#output
Before unset: first second third
After unset:

The same way we can specify and index to be unset:

arrayA=('first' 'second' 'third')
printf "%s\n" "Before deleting index [1]: ${arrayA[@]}"
unset 'arrayA[1]'
printf "%s\n" "After deleting index [1]: ${arrayA[@]}"

# output:
Before deleting index [1]: first second third
After deleting index [1]: first third

Notice that removing an item from an array index leaves gaps in it. Let's take a look at the example above checking its index:

arrayA=('first' 'second' 'third')
printf "%s\n" "Index before unset[1]: ${!arrayA[@]}"
unset 'arrayA[1]'
printf "%s\n" "Index after unset[1]: ${!arrayA[@]}"

# output:
Index before unset[1]: 0 1 2
Index after unset[1]: 0 2

If any elements have been removed from an array or we just want to recreate indices without gaps, we can re-index the array:

arrayA=("${arrayA[@]")

— Arrays can be accessed via its index values. They can also be iterated using while, for and foreach loops.

  • Access an element from specific index in an array:
# where n is a numeric value
${array[n]} 
${array:n}
  • Access all elements inside an array:
${array[@]} 
  • Access all elements except marked one:
# where n is a numeric value
${array[@]:1} 
  • Access all elements within a range inside an array:
# where n and m are numeric values
${array[@]:n:m} 

# example
${array[@]:1:4} 
  • Check the number of elements inside an array:
${#array[*]} 
${#array[@]}
  • Check the length of an specific element inside an array:
# where n is a numeric value
${#array[n]} 
  • Using a for loop to iterate an array:
for i in "${array[@]}"; do
  #commands for $i
done

Functions

Writing down each action line on a script file is fine, but when we start collecting several lines of code, or we want to reuse some functionality with another process, we have to start thinking in a way to reuse code and make it readable and manageable. Functions allow us to organize logic into blocks that we can manage and reuse in a comfortable way.

A basic function structure looks like this:

askUser ()
{
   printf "%s" "greetings, please type your user name: "
   read user
   printf "%s\n" "$user is your current user name"
}

however if our function is small enough to be displayed in one line we can do so, remembering that in one-line functions commands need to ended with a semi-colon:

askUser () {read -p "type your name: " user ; echo "Hi, $user";}

Now each time we want to execute the code inside a function we only need to call the function by its name, without any decorations:

askUser

Functions need to be created before we can call them to be executed.

— We can pass arguments to functions inside shell scripting by adding them after calling the function.

To call arguments inside the function follow the scheme $1 $2 ... $n.

Those arguments can be both fulfilled inside our code or as a user input arguments when running the script.

greetUser ()
{
   printf "%s\n" "greetings, $1"
}
greetUser

In this first example we will need to type an argument after calling the script:

# output
$ ./greetUser.sh $USER
$ greetings, Mike

Now let's use a value inside the code to act as an argument for our function:

activeUser=$USER

greetUser ()
{
   printf "%s\n" "greetings, $1"
}
greetUser "$activeUser"

This way we only need to run our script without typing any extra argument:

# output
$ ./greetUser.sh
$ greetings, Mike

— We can return values from a function too in a few ways.

  • Change the state of a variable/s.
  • Print output to stdout.
  • Run exit command to end the script.
  • Run return command to end the function and optionally return a value.

Summing up

While Shell scripting has some limits compared with some other modern scripting languages, it's pretty easy to use it and it can cover almost all the needs to do system management, plus in special places like servers one can face a situation where the only available stuff to work with is a command-line text editor and a shell.