DEV Community

Cover image for Shellscripting: Functions
Darkø Tasevski
Darkø Tasevski

Posted on • Edited on

Shellscripting: Functions

This is part three of my post series about Shellscripting, you can check out previous posts here:


For functions we can say that they're shellscripts within shellscript.

One of the main reasons why we are using functions is to follow the DRY principle, which means that we should write a function once, and then we can use it many times. This can sometimes drastically reduce the script length, and also it's much easier to maintain as we have a single function to edit and troubleshoot.

Function:

  • Hides implementation details from the main script, simplifying the shell script’s main body of code
  • Can be replaced if the underlying detail it works with is changed
  • Can be tested over and over again as a small piece of a larger script, with changing input values to prove that the code is correct
  • Allows consistent reuse of code from within the script, or even between several shell scripts.

xkcd

Slightly relevant xkcd 😄

Exit statuses

Before we dive into functions it's essential to know that every command executed in shell returns an exit status in the range of 0 to 255.
De facto status for success is 0, all others are codes for an error condition.
This codes can be used in scripts for throwing and checking errors. Usually, we can find what various exit statuses mean by checking the documentation for that error code or look into the source code.

We can use $? to check the exit status of the previously executed command.

$ ls dir/
$ echo $? 
Enter fullscreen mode Exit fullscreen mode

If dir exists echo $? will return 0 status code, otherwise, it should return 2, the error code for directory not found.

We can explicitly define the return codes with exit command:

#! bin/bash
HOST="google.com"

ping -c 1 $HOST

if ["$?" -ne "0"]
then
    echo "$HOST unreachable"
    exit 1
fi
exit 0
Enter fullscreen mode Exit fullscreen mode

Simply use the exit command in your script and follow it with the integer in the range of 0 to 255. If we do not specify the return code with the exit command, then the exit status of the previously executed command in the shellscript will be used as the exit status. This is also true if we do not include exit command at all.

Whenever the exit command is reached the shellscript will stop running.

All functions have an exit status. We can explicitly return status within the function with return keyword:

function myFunc() {
    return 1 # returning exit status code 1
}
Enter fullscreen mode Exit fullscreen mode

Also, status can be returned implicitly with the exit status of the last command executed in the function.


Positional parameters

Positional parameters are the variables which we can use to specify arguments passed to the function via command line. For example, if we execute the script like this:

script.sh param1 param2 param3

Inside that script we can access all command line arguments like this:

$0: "script.sh" # $0 is always the name of the script
$1: "param1" # $1 is the first parameter,
$2: "param2" # $2 is the second
$3: "param3" # $3 is the third, and so on...
Enter fullscreen mode Exit fullscreen mode

NOTE: You cannot change the values of these variables

A practical example:

# args.sh
#!/bin/bash
echo "I was called as $0"
echo "My first argument is: $1"
echo "My second argument is: $2"
echo "I was called with $# parameters."
Enter fullscreen mode Exit fullscreen mode
$ ./args.sh one two
I was called as ./args.sh
My first argument is: one
My second argument is: two
I was called with 2 parameters.
Enter fullscreen mode Exit fullscreen mode

We can use $# to check with how much parameters script was called, which is a good way to check if the user has executed the script with enough number of args, for example:

$ cat argCheck.sh
#!/bin/bash
if [ "$#" -eq "2" ]; then
    echo "The script was called with exactly two parameters. Let’s continue."
else
    echo "You provided $# parameters, but 2 are required."
Enter fullscreen mode Exit fullscreen mode

We can use $@ variable when we want to loop through script parameters:

# This will loop through all parameter passed to the script when executed
for USER in "$@"; do
    passwd -l $USER # lock the account
done
Enter fullscreen mode Exit fullscreen mode

Creating a function

Let's get back to the functions.
It's important to note that function must be defined before it's called, it is conventional to define functions at the start of the file, although this is not strictly necessary.
The block of code defined as a function can be declared in one of three different ways, depending on the exact shell in use. The standard Bourne shell syntax uses the function name followed immediately by a pair of parentheses () and curly brackets {} around the code itself:

# The most common way
myFunc() {
    # Code
    echo "Hello"
}
# function keyword is optional
# so this is also correct
function mySecondFunc () {
    # More code
    echo "World"
}
Enter fullscreen mode Exit fullscreen mode

There is a third syntax, which is not accepted by the Bourne shell, although bash and ksh both accept it. Instead of following the function name by a pair of parentheses, the function name is preceded by the keyword function:
function myFunc

As far as I've learned so far, the first one (without keyword function) is the most commonly used as it's accepted by all shells. The second syntax is also used frequently and by using the function keyword, it provides a more clear declaration that it is a function. Regarding the 3rd one, I couldn't find any information about it, except that it exists :|

Calling a function

We can call and execute the function by simply typing its name in the script (after it's been defined first):

function hello() {
    echo "Hello World!"
}

hello
Enter fullscreen mode Exit fullscreen mode
function hello() {
    echo "Hello ${1}!"
}

hello World # Output: Hello World
Enter fullscreen mode Exit fullscreen mode

Functions can also call other functions. Here we can see there that the hello function calls now function before it's declared, but that's okay as the now function gets read into the script before the hello function is called, so in the order of the execution it's defined before it's used.

function hello() {
    now
}
function now() {
    echo "$(date +%r)"
}

hello # Output: 02:15:36 PM
Enter fullscreen mode Exit fullscreen mode

But, for example, something like this won't work:

function hello() {
    now
}
hello # hello is called before now is defined
function now() {
    echo "$(date +%r)"
}
Enter fullscreen mode Exit fullscreen mode

Besides calling other functions, shell functions can also call themselves recursively. A simple example to demonstrate this is the mathematical factorial function.

$ cat factorial.sh 
#!/bin/bash

function factorial() {
    if [ "$1" -gt "1" ]; then 
        previous=`expr $1 - 1` 
        parent=`factorial $previous` 
        result=`expr $1 \* $parent` 
        echo $result
    else
        echo 1
    fi
}
factorial $1
$ ./factorial.sh 6 720
Enter fullscreen mode Exit fullscreen mode

You should be very careful when working with recursive functions tho, especially if you are creating files in them, you could end up with more open files than allowed by the system.

Functions also have positional parameters, and $@ can also be used to retrieve the list of all passed arguments.

# $0 is the script itself, not the function name
function helloFunc(){
    echo "Hello ${1} ${2}!"
}
helloFunc Shell World
# Output: Hello Shell World!
Enter fullscreen mode Exit fullscreen mode

Functions also have access to all global variables. But, as a reminder, it's the best practice to use only local variables inside the functions to avoid side effects, which can eventually cause bugs.


There is much more to be said about the functions in shellscript, so consider this post just as a small introduction to their usage in scripting. In the next post, I'll write a bit about Wildcards, Character Classes and about logging and debugging shellscript.
Thanks for reading!

Top comments (10)

Collapse
 
ikirker profile image
Ian Kirker

One veeeery useful and slightly weird thing to add here is that $*, "$*", $@, and "$@" all mean "all the arguments", but get expanded in different ways.

Here's an example:

#!/bin/bash

function print_args() {
    echo "-- Unquoted, asterisk --"
    for i in $*; do
        echo $i
    done

    echo "-- Quoted, asterisk --"
    for i in "$*"; do
        echo $i
    done

    echo "-- Unquoted, atpersand --"
    for i in $@; do
        echo $i
    done

    echo "-- Quoted, atpersand --"
    for i in "$@"; do
        echo $i
    done
}

print_args "a" "b c" "d e f"
Enter fullscreen mode Exit fullscreen mode

gives:

-- Unquoted, asterisk --
a
b
c
d
e
f
-- Quoted, asterisk --
a b c d e f
-- Unquoted, atpersand --
a
b
c
d
e
f
-- Quoted, atpersand --
a
b c
d e f
Enter fullscreen mode Exit fullscreen mode

The quoted atpersand ("$@") option is the only option that preserves argument groupings including spaces, while "$*" treats all the arguments as one string, and $* and $@ both expand to the fully split, ungrouped, unquoted arguments.

"$@" behaves as if it were expanded with each element quoted separately, rather than "$*" which behaves as if it were expanded and then the whole expansion quoted.

(Note that this is the same as the behaviour for using arrays, in "${array[*]}" vs "${array[@]}".)

Collapse
 
puritanic profile image
Darkø Tasevski

I would agree on weird 😄but interesting nevertheless.
I didn't know about $* nor that quoting/unquoting can have such different results. Going to take note of this, thanks for sharing!

Collapse
 
moopet profile image
Ben Sinclair

Just to be picky about positionals, when you say

NOTE: You cannot change the values of these variables

You kinda can, because you can use shift to reposition them,

foo() {
  echo $1, $2, $3
  shift
  echo $1, $2, $3
}

foo one two three four five

# one, two, three
# two, three, four
Enter fullscreen mode Exit fullscreen mode
Collapse
 
puritanic profile image
Darkø Tasevski

Each day you learn something new :D Thanks!

Collapse
 
vlasales profile image
Vlastimil Pospichal
#!/bin/bash
for i in $@; do
    echo "$i"
done

$ ./args.sh alfa "beta gamma"
alfa
beta
gamma
Enter fullscreen mode Exit fullscreen mode

That's wrong!

Right version:

#!/bin/bash
for i in "$@"; do
    echo "$i"
done

$ ./args.sh alfa "beta gamma"
alfa
beta gamma
Enter fullscreen mode Exit fullscreen mode
Collapse
 
puritanic profile image
Darkø Tasevski

Whoops, missed that one, thanks for noticing!

Collapse
 
polyluxus profile image
Martin Schwarzer

You might want to check out Shellcheck to avoid simple errors, probably unwanted behaviours, and deprecated code, and maybe even more. The online wiki is really helpful in explaining why things may go wrong, or why some code has been replaced.

Collapse
 
ferricoxide profile image
Thomas H Jones II

Shellcheck is a great to add to a Git probject's .travis.yml (if you're using Travis.CI as a test-framework for commits, obvs.; otherwise call it from whatever else you're using to test uploaded code).

Collapse
 
avalander profile image
Avalander

I've been waiting for this post my whole life, thank you so much for sharing!

Collapse
 
bernhardwebstudio profile image
Bernhard Webstudio

Hmmm. functions might be a better way than my current approach: just moving repeated instructions into a new shell script which I invoke when I need the "function" 😁