This is part 23 of a series on how to approach bash programming in a way that’s safer and more structured than your basic script.

See part 1 if you want to catch the series from the start.

Last time, we discussed naming and namespaces. This time, let’s discuss passing arguments to functions.

Functions in bash don’t carry a signature or prototype for their arguments. Instead, any arguments to the function appear as positional arguments in the context of the function.

By definition, positional arguments are already local to the function, so you don’t need to do anything special to work with them. However, you can assign them to other variables if you want to be more descriptive, or if you want to free up the positional argument array for other purposes. If you do, just remember to declare the named variables locally.

Strings and Arrays and Hashes, Oh My

Basic types, which I’ll just refer to as strings from here on out since I don’t really use integers, are the only argument type supported in bash.

Since positional arguments are just an array of strings, bash easily handles passing strings to functions. If we’re only talking about passing a single array, then bash can handle that easily as well, by expanding the array when calling the function.

However, bash cannot easily handle passing multiple arrays, nor hashes.

For this reason, most people handle these kinds of arguments not as arguments, but rather as global variables. If the array or hash is stored in a global variable, the function doesn’t need to receive it as an argument.

This works if you write both the function and the script in which it’s used. It requires shared knowledge of the variable name between the function and the code which owns the global namespace.

It’s not a method for writing reusable code, however. The point of having arguments is so that functions and the code which calls them do not share or have to know about each other’s variable names. This makes them independent from each other.

So there should be another way to address the issue of passing hashes and arrays as arguments to a function, one which uses the conventional bash method of passing arguments. We’ll address this issue in another blog entry.

Passing a Single Array

Before we get into anything more complicated, we can briefly discuss the case of a single array as argument.

If all you have to pass is an array, then as I mentioned before, you can simply expand it:

myfunc () {
  echo "Arguments are $@"
}

myarray=( one two three )

myfunc ${myarray[@]}

If you have other arguments than the array, you can still pass them and the array the usual way, so long as you pass the array last. This lets the array be an arbitrary length, and lets you take advantage of the shift commmand:

myfunc () {
    local arg1=$1; shift
    local arg2=$1; shift

    echo "arg1 is $arg1"
    echo "arg2 is $arg2"
    echo "array is $@"
}

myarray=( one two three )

myfunc val1 val2 ${myarray[@]}

Default Arguments

In many languages, you can specify a default value for an argument by supplying it in the function signature.

Bash doesn’t have a function signature per se, so you have to define defaults differently, but it’s still straightforward:

myfunc () {
  local arg=${1-default value}

  echo "Argument is $arg"
}

If you have several arguments, you can easily provide several defaults:

myfunc () {
  local arg1=${1-default value1}
  local arg2=${2-default value2}

  echo "Argument 1 is $arg1"
  echo "Argument 2 is $arg2"
}

This leads us to a problem, however. With the above function, I can’t supply an argument for arg2 if I want to use the default value for arg1:

# Doesn't work
myfunc val_for_arg2

We could change local arg1=${1-default value1} to have a colon, as in local arg1=${1:-default value1}. Then we could feed in an empty string for arg1 to get the default value:

myfunc '' val_for_arg2

This would work, since the expansion with a colon will replace an empty string with the default value. However, that would prevent an empty string from being a valid value to pass for that argument, which may or may not be acceptable.

Instead, if I have more than one optional argument, I prefer to make all of them be keyword arguments. This lets you choose which optional arguments to supply without having to worry about the other default values. They can also be supplied in any order then too.

Simple Keyword Arguments

Bash doesn’t support keyword arguments natively. We could come up with a sophisticated implementation, but that’s not the point of this blog. Instead, we’ll do the simplest possible thing that could work.

The idea is to have a local variable within the function whose value is set to a default, and then to accept an argument which consists of that variable name, an equals sign and a value supplied by the user.

Since such an argument already has the format of an assignment in bash, we could just iterate through the keyword arguments and eval them individually. Better still, we could eval them together as a single string, since bash allows multiple assignments on one line.

Two observations: first, eval‘ing the assignments would create globals if we didn’t force it with the local keyword. While we may have already declared locals for the arguments when specifying default values, the caller isn’t constrained from feeding our function other keyword arguments which don’t correspond and won’t be masked. We want to ensure this doesn’t happen, whatever the caller passes.

Second, the local keyword takes multiple assignments as well, and even better, it accepts expansions. The following works:

myfunc () {
  local required_arg1=$1; shift
  local keyword_arg2=default_value
  local $@

  echo "Required argument 1 is $required_arg1"
  echo "Keyword argument 2 is $keyword_arg2"
}

myfunc val1 keyword_arg2=val2 keyword_arg3=val3

One thing to notice here is that all keyword arguments must come after the required arguments so that the required ones may be shifted and the keywords can be fed to local as $@.

Unfortunately, if no keyword arguments are supplied, then local changes its behavior instead to listing the current locals on stdout. So we need to test for remaining arguments before calling it. Since that gets a bit uglier looking, let’s make an alias called kwargs:

shopt -s expand_aliases
alias kwargs='(( $# )) && local'

myfunc () {
  local required_arg1=$1; shift
  local keyword_arg2=default_value
  kwargs $@

  echo "Required argument 1 is $required_arg1"
  echo "Keyword argument 2 is $keyword_arg2"
}

There you go, poor-man’s keyword arguments in bash.

Continue with part 24 - passing arrays.