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.
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 $?
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
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
}
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...
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."
$ ./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.
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."
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
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"
}
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
function hello() {
echo "Hello ${1}!"
}
hello World # Output: Hello World
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
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)"
}
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
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!
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)
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:
gives:
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[@]}"
.)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!Just to be picky about positionals, when you say
You kinda can, because you can use
shift
to reposition them,Each day you learn something new :D Thanks!
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.
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).
That's wrong!
Right version:
Whoops, missed that one, thanks for noticing!
I've been waiting for this post my whole life, thank you so much for sharing!
Hmmm.
function
s 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" 😁