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 usingfor
andwhile
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 thefor
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.