# Bash Script with a Library for Named Arguments

**Date:** 2022-12-22  
**Author:** Kees C. Bakker  
**Categories:** bash  
**Original:** https://keestalkstech.com/bash-script-with-a-lib-for-named-parameters/

![Bash Script with a Library for Named Arguments](https://keestalkstech.com/wp-content/uploads/2022/12/gabriel-heinzer-xbEVM6oJ1Fs-unsplash.jpg)

---

When working with Bash scripts, it is often useful to make them more flexible by using parameters. In a previous blog post, [I demonstrated how to add named parameters to a single script](https://keestalkstech.com/named-arguments-in-a-bash-script/). If you want to apply this feature to multiple scripts, it makes sense to move the parser to a separate library and source it where needed. Let's see how.

## Initial setup

To get started, create `./lib/arguments.sh` and `test.sh`. Only `test.sh` needs to be executable:

```sh
mkdir -p ./lib
touch ./lib/arguments.sh ./test.sh
chmod +x ./test.sh
```

Next, open `test.sh` in your preferred text editor and set it up as follows:

```sh
#!/usr/bin/env bash

set -euo pipefail

script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
source "$script_dir/lib/arguments.sh"
```

`arguments.sh` is sourced into the current shell. It does not need a shebang or executable permissions, and the calling script remains responsible for configuring shell options.

## Named arguments function

First, add the library helpers to `./lib/arguments.sh`. They validate names against the caller's whitelist, check optional functions, and report errors consistently:

```sh
function_exists() {
    declare -F "$1" >/dev/null
}

die() {
    printf "Script failed: %s\n" "$1" >&2
    exit 1
}

is_allowed_argument() {
    local candidate=$1
    local allowed

    for allowed in "${allowed_arguments[@]}"; do
        if [[ $candidate == "$allowed" ]]; then
            declare -p -- "$candidate" &>/dev/null
            return
        fi
    done

    return 1
}
```

Next, add `parse_input_args`. It accepts only variables that the caller has both declared and listed in `allowed_arguments`. Because the parser runs inside a function, it uses `printf -v` to update the existing caller variable:

```sh
parse_input_args() {
    while [ $# -gt 0 ]; do
        if [[ $1 == "--help" || $1 == "-h" ]]; then
            function_exists usage || die "Function usage is not defined"
            usage
            exit 0
        elif [[ $1 == "--version" || $1 == "-v" ]]; then
            function_exists version || die "Function version is not defined"
            version
            exit 0
        elif [[ $1 == "--"* ]]; then
            if ! is_allowed_argument "${1#--}"; then
                die "Unknown parameter: $1"
            fi

            if (( $# < 2 )); then
                die "Missing value for $1"
            fi

            if [[ $2 == "--"* ]]; then
                die "Missing value for $1"
            fi

            printf -v "${1#--}" "%s" "$2"
            shift 2
        else
            die "Unexpected positional argument: $1"
        fi
    done
}
```

The caller declares regular variables with their defaults and lists the supported names in a readonly `allowed_arguments` array. An argument is accepted only when both conditions are met. Values cannot start with `--`.

Let's declare two variables in `test.sh`, whitelist their names, and see how it works:

```sh
declare env="" tag="nothing"
declare -ar allowed_arguments=(env tag)

parse_input_args "$@"

printf "Env: %s\n" "$env"
printf "Tag: %s\n" "$tag"
```

Now we can test the defaults and override them with named arguments:

```sho
$ ./test.sh
Env:
Tag: nothing

$ ./test.sh --env dev --tag hello
Env: dev
Tag: hello

$ ./test.sh --help
Script failed: Function usage is not defined
```

The last command fails deliberately: supporting `--help` requires the calling script to provide a `usage` function. We'll add one in [the elaborate example](#a-more-elaborate-example).

## Required parameter validation

Some parameters may need to be non-empty. Add the following helper to `./lib/arguments.sh`:

```sh
ensure_required_input_arg() {
    local name=$1
    local value=$2

    if [[ -z $value ]]; then
        function_exists usage && usage
        die "Missing parameter $name"
    fi
}
```

To make both arguments required, initialize them as empty strings and validate them before using their values:

```sh
declare env="" tag=""
declare -ar allowed_arguments=(env tag)

parse_input_args "$@"

ensure_required_input_arg "--env" "$env"
ensure_required_input_arg "--tag" "$tag"

printf "Env: %s\n" "$env"
printf "Tag: %s\n" "$tag"
```

Missing options and missing values now fail at the appropriate stage:

```sho
$ ./test.sh --env dev
Script failed: Missing parameter --tag

$ ./test.sh --env dev --tag
Script failed: Missing value for --tag

$ ./test.sh --env dev --tag hello
Env: dev
Tag: hello
```

You can also add a dedicated [`usage` function that explains which input the script requires](https://keestalkstech.com/named-arguments-in-a-bash-script/#argument-validation).

## Final arguments.sh file

Our complete `./lib/arguments.sh` file now looks like this:

```sh
function_exists() {
    declare -F "$1" >/dev/null
}

die() {
    printf "Script failed: %s\n" "$1" >&2
    exit 1
}

is_allowed_argument() {
    local candidate=$1
    local allowed

    for allowed in "${allowed_arguments[@]}"; do
        if [[ $candidate == "$allowed" ]]; then
            declare -p -- "$candidate" &>/dev/null
            return
        fi
    done

    return 1
}

parse_input_args() {
    while [ $# -gt 0 ]; do
        if [[ $1 == "--help" || $1 == "-h" ]]; then
            function_exists usage || die "Function usage is not defined"
            usage
            exit 0
        elif [[ $1 == "--version" || $1 == "-v" ]]; then
            function_exists version || die "Function version is not defined"
            version
            exit 0
        elif [[ $1 == "--"* ]]; then
            if ! is_allowed_argument "${1#--}"; then
                die "Unknown parameter: $1"
            fi

            if (( $# < 2 )); then
                die "Missing value for $1"
            fi

            if [[ $2 == "--"* ]]; then
                die "Missing value for $1"
            fi

            printf -v "${1#--}" "%s" "$2"
            shift 2
        else
            die "Unexpected positional argument: $1"
        fi
    done
}

ensure_required_input_arg() {
    local name=$1
    local value=$2

    if [[ -z $value ]]; then
        function_exists usage && usage
        die "Missing parameter $name"
    fi
}
```

## A more elaborate example

Now that the library is in place, let's create a larger script with `usage` and `version` functions and two required parameters:

```sh
#!/usr/bin/env bash

set -euo pipefail

script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
source "$script_dir/lib/arguments.sh"

declare repo="" branch=""
declare -ar allowed_arguments=(repo branch)
program_version="0.0.1"

usage() {
    echo ""
    echo "Performs a shallow clone of a GitHub repository with a GitHub app."
    echo ""
    echo "usage: $0 --repo string --branch string [--help] [--version]"
    echo ""
    echo "  --repo string    name of the repository"
    echo "                   (example: KeesCBakker/keestalkstech-code-gallery)"
    echo "  --branch string  branch to clone"
    echo "                   (example: main)"
    echo "  --version        show the version of the script"
    echo "  --help           show this help"
    echo ""
}

version() {
    echo "$program_version"
}

parse_input_args "$@"

ensure_required_input_arg "--repo" "$repo"
ensure_required_input_arg "--branch" "$branch"

printf "Repository: %s\n" "$repo"
printf "Branch: %s\n" "$branch"
```

That's it! The caller controls defaults with regular variables and explicitly whitelists which names the library may update.

## Changelog

- 2026-07-19: Restricted parsing to declared variables in an explicit caller whitelist, added missing-value validation, and clarified sourced-library behavior.
- 2025-01-22: Added a better example and support for `--version`, `-v`, and `-h`.
- 2022-12-22: Initial article.
