108

I am trying to learn how to use getopts so that I can have scripts with parsed input (although I think getopts could be better). I am trying to just write a simple script to return partition usage percentages. The problem is that one of my bash functions does not seem to like that I reference $1 as an variable within the function. The reason I reference $1 is because the get_percent function can be passed a mount point as an optional argument to display instead of all of the mount points.

The script

#!/usr/bin/bash

set -e
set -u
set -o pipefail

get_percent(){
    if [ -n "$1" ] 
    then
        df -h $1 | tail -n +2 | awk '{ print $1,"\t",$5 }'
    else
        df -h | tail -n +2 | awk '{ print $1,"\t",$5 }'
    fi
}

usage(){
    echo "script usage: $(basename $0) [-h] [-p] [-m mount_point]" >&2
}

# If the user doesn't supply any arguments, we run the script as normal
if [ $# -eq 0 ];
then
    get_percent
    exit 0
fi
# ...

The Output

$ bash thing.sh
thing.sh: line 8: $1: unbound variable

$ bash -x thing.sh
+ set -e
+ set -u
+ set -o pipefail
+ '[' 0 -eq 0 ']'
+ get_percent
thing.sh: line 8: $1: unbound variable
4
  • I don't think this has anything to do with getopts, does it? Your script exits due to -u before calling getopts.
    – ilkkachu
    Commented Aug 16, 2018 at 18:31
  • @ikkachu no I guess it doesn't. But I'm not sure I can change the title now. Commented Aug 16, 2018 at 18:33
  • There should be that small "edit" text under the post, just beneath the tags in a question
    – ilkkachu
    Commented Aug 16, 2018 at 18:36
  • You might as well use set -o errexit -o nounset -o pipefail (or set -o{errexit,nounset,pipefail}) rather than using half single-letter and half long options. Then it would have made it more obvious what the problem was. Commented May 9 at 19:12

5 Answers 5

141

set -u, short for set -o nounset sets the nounset option which causes the shell to abort exactly as you describe if you reference a variable (though that extends to other types of parameters such as the $1 positional parameter) which has not been set. You are invoking your script with no arguments, so get_percent is being invoked with no arguments, causing $1 to be unset.

Either check for this before invoking your function (with if [ "$#" -gt 0 ]), or use default expansions (${1-default} will expand to default if not already set to something else; not to be confused with ${1:-default} which expands to default if $1 it not set or is empty like when your script is called as your-script '' 'goes into $2' 'goes into $3').

In any case, your df -h $1 should be df -h -- "$1" if the intention is to have df report the disk usage for the path or device given in the first positional parameters.

5
  • 31
    In particular, one could use [ -n "${1-}" ] (that is, with an empty default value) to see if the parameter is set and non-empty; or [ "${1+x}" = x ] to see if it's set, even if empty.
    – ilkkachu
    Commented Aug 16, 2018 at 18:30
  • I still get unbound variable error despite using if [[ -n ${1-default} ]] Commented Dec 7, 2019 at 2:59
  • 10
    @ChaitanyaBapat I was still getting unbound variable as well until I used :- instead of -. So, for me at least, the ${1:-default} no loner raised the error. Commented Apr 9, 2020 at 11:26
  • As a side note, if you are trying to access an array element with an index out of range, you get the same error (with syntax ${my_array[$index]}). Commented Dec 3, 2021 at 23:16
  • @AdamBadura, I checked with bash versions as far back as 2.0 from 1996, and none of them reported unbound variable for bash -uc 'echo "${1-default}"'. That would an obvious bug if it did. In any case using ${1:-default} is wrong as as it expands to default when $1 is set, if that's to an empty string, so if there was such a bug, you'd have to work around it some other way such as with if [ "$#" -gt 0 ]; then arg=$1; else arg=default; fi. Commented May 9 at 19:42
26

This is the effect of set -u.

You could check $# inside the function and avoid referencing $1 if it is not set.

With $# you can access the number of parameters. In global context it is the number of parameters to the script, in a function it is the number of parameters to the function.

In the context of the question, it is

if [ $# -ge 1 ] && [ -n "$1" ]
then
    df -h $1 | tail -n +2 | awk '{ print $1,"\t",$5 }'
else
    df -h | tail -n +2 | awk '{ print $1,"\t",$5 }'
fi

Note that you have to use [ $# -ge 1 ] && [ -n "$1" ] and not [ $# -ge 1 -a -n "$1" ], because that would first evaluate $1 and then check $#.

0
12

Other answers have mentioned that this is the effect of set -u. None of the other answers have mentioned that set -u can be reversed with set +u:

$ echo $VAR  # nothing echoed below as VAR is unset:

$ set -u
$ echo $VAR
bash: VAR: unbound variable
$ set +u
$ echo $VAR  # nothing echoed below as VAR is unset:

7

With "set -u" in the script any reference to an unset variable will cause an error, especially testing if it has a value, which is what the script is doing.

The following function will test for unset variable without tripping the set -u:

if:IsSet() {
  [[ ${!1-x} == x ]] && return 1 || return 0
}

Using it:

if:IsSet someVariableName || echo "someVariableName is not set"

As for the actual function, test whether an argument was passed in, not whether it is empty. This will avoid the unset variable issue without you taking any special steps, like testing whether the variable has a value.

get_percent(){
    if [ ${#1} -eq 0 ] 

BTW: Colon in a function name is just a character. It is valid in a function name in Bash, as are %, }, {, [, and a bunch of others :-)

2
  • 2
    This does not work if someVariableName happens to be set to 'x'. You could fix it by using [[ ${!1-x} == x ]] && [[ ${!1-y} == y ]] && return 1; return 0 in your if:IsSet function. Commented Jan 6, 2022 at 14:49
  • as @ilkkachu mentioned in another comment, you would be better off using [ "${!1+x}" = "x" ]. This bash parameter expansion will expand to x if and only if the name/symbol provided by !1 is set (that is, null (empty string) or any string of positive length).
    – cubernetes
    Commented Aug 26, 2023 at 21:04
6

Since this is bash you can sidestep the check for $1 being set and just use "$@" ($1 is the first parameter, $@ is all of them; when double-quoted, it disappears completely if it has no values, which avoids it being caught by set -u):

get_percent() {
    df -h "$@" | awk 'NR>1 { printf "%s\t%s\n", $1, $5 }'
}

I've also tweaked the rest of the line slightly so that you don't get {space}{tab}{space} between the two values you output but insead you get just a {tab}. If you really want the two invisible spaces then change the awk to use printf "%s \t %s\n", $1, $5.

6
  • I will have to look into this. I'm not familiar with that variable type. Thanks Commented Aug 16, 2018 at 18:29
  • @TimothyPulliam I've added a short explanation of $@ for you Commented Dec 7, 2019 at 9:34
  • 1
    Actually, since it's bash and since nounset is set, note that in some older versions of bash, $@ expansion was also tripping on nounset when $# is 0, and you had to work around if with ${1+"$@"} (for a different reason than the one for which you needed that for old versions of the Bourne shell). See in-ulm.de/~mascheck/various/bourne_args for details. Commented May 9 at 19:52
  • @StéphaneChazelas seeing as nounset is involved, and the workaround is needed for some versions of even version 4 of bash, am I correct in understanding that the action line should therefore be df -h ${1+"$@"} | awk…? Commented May 9 at 20:04
  • More df -h -- ${1+"$@"} or assume that systems would be either running bash versions newer than 4.0.x or if they still ran 4.0.x, would have at least applied patch 28 from 2009 Commented May 9 at 20:17

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .