Bash

Style

Function Parameters

How to specify bash functions with required parameters. When the function is applied to the wrong number of arguments, a non-0 exit status should be returned.

One important question: is the empty string "" a valid parameter?

Using [[ (new-test)

The double-bracket [[ syntax is called "new-test".

-n is a string comparison operator that tests if a string is not null zero-length.

foo() {
    [[ -n "$1" ]] || {
        echo "Error: param 1 is required"
        return 1
    }
    echo "Param is: $1"
}

foo
foo ""
foo bar

Using [ (test)

Same as above, but not as good. For more info, see FAQ: "What is the difference between test, [ and [[?"

foo() {
    [ -n "$1" ] || {
        echo "Error: param 1 is required"
        return 1
    }
}

Note that [ ] is an alias for test. The above example could be written as:

foo() {
    a=${1-}
    test -n "$a" || {
        echo "Error: a is required"
        return 1
    }
}

if-else

foo() {
    if [ -z ${1+x} ]; then
        echo "Error: param 1 is required"
        return 1
    else
        local a=$1
    fi
    echo "a is '$a'"
}

foo
foo ""
foo bar

Set

set -u

Without set -u this works fine and prints nothing, even though foo is not bound.

echo $foo

This produces an error: "bash: line 2: foo: unbound variable" (TODO: org mode to print this).

set -u
echo $foo

Works with eval.

set -u
vars="FOO=foo BAR=bar"
eval $vars
echo $FOO

Script Arguments

Same patterns as function parameters can be used.

if [ -z ${1+x} ]; then
    echo "arg 1 (foo) is required"
    exit 1
fi
foo=$1
echo "foo is: '$foo'"

Script Options

getopts

See home/bin/ssm script for a good example.

showHelp() {
    cat <<END
Usage: $(basename "$0") [-p profile]
END
}

while getopts :hp: arg; do
    case ${arg} in
        h)
            showHelp
            exit 0
            ;;
        p)
            profile="$OPTARG"
            ;;
        \?)
            echo "Invalid option: -$OPTARG" >&2
            exit 1
            ;;
        :)
            echo "Option -$OPTARG requires an argument" >&2
            exit 1
            ;;
    esac
done
shift $((OPTIND -1))

# Define default option values
profile=${profile:="chrisc"}

aws="aws --profile $profile --region us-east-1"

Heredocs

Whitespace

Use <<-. Each line in the HEREDOC has to begin with a TAB. Having trouble getting this working with org-babel.

foo="bar"
a=$(cat <<-EOF
hello
$foo
EOF
)
echo "$a"

Interpolation

A heredoc can interpolate variable names to values, or the whole heredoc can be taken completely literally without any interpolation. The difference is whether the limit string has single quotes around it.

with interpolation

foo="fooo"
a=$(cat <<EOF
hello
$foo
EOF
)
echo "$a"

Note, this also works with single quotes!

foo="fooo"
a=$(cat <<EOF
hello
'$foo'
EOF
)
echo "$a"

without interpolation

foo="bar"
a=$(cat <<'EOF'
hello
$foo
EOF
)
echo "$a"

Verify Environment Variables

Verify some environment variables exist.

: "${FOO:?FOO is not set}"
: "${BAR:?BAR is not set}"

Parameter Expansion

Resources

:-

Docs for ${parameter:-[word]}

If parameter is unset or null, the expansion of word (or an empty string if word is omitted) shall be substituted; otherwise, the value of parameter shall be substituted.

var=""
if [ -z ${var:-x"} ]; then
    echo "var is unset"
else
    echo "var is set to '$var'"
fi
if [ -z ${var:-"foo"} ]; then
    echo "var is unset"
else
    echo "var is set to '$var'"
fi

+

Docs for ${parameter:+[word]}

If parameter is unset or null, null shall be substituted; otherwise, the expansion of word (or an empty string if word is omitted) shall be substituted.

-z is a comparison operator that tests if the argument is null or empty.

var=""
if [ -z ${var+x} ]; then
    echo "var is unset"
else
    echo "var is set to '$var'"
fi

In this case, var is the empty string, which is not "unset or null". Therefore the + parameter expansion expands this to x (which passes the -z check).

if [ -z ${var+x} ]; then
    echo "var is unset"
else
    echo "var is set to '$var'"
fi

In this case, var has not been declared, so it is "unset or null". The + parameter expansion expands it to null.

var=""
if [ -z ${var+} ]; then
    echo "var is unset"
else
    echo "var is set to '$var'"
fi

In this case we leave the word spot empty in the parameter expansion, so if var is empty, it is subsituted by an empty string. Now the -z test sees an empty argument, and this prints out var is unset.

##

Remove largest prefix pattern.

a_list=("foo/aa.foo" "bar/ab.foo" "baz/ac.foo")
for a in ${a_list[@]}; do
    # The "*/" is a regex
    echo ${a##*/}
done

Arrays

* vs @

See this StackOverflow answer.

The difference between [@] and [*]-expanded arrays in double-quotes is that "${myarray[@]}" leads to each element of the array being treated as a separate shell-word, while "${myarray[*]}" results in a single shell-word with all of the elements of the array separated by spaces (or whatever the first character of IFS is).

Usually, the [@] behavior is what you want.

Basic

arr=("foo" "bar" "baz")
echo ${arr[@]}
arr=("foo" "bar" "baz")
for x in "${arr[@]}"; do
    echo $x
done

Length

arr=("foo" "bar" "baz")
echo ${#arr[@]}

Pipe array to xargs

Start a subprocess per element in an array, where each subprocess runs $script.

script=$(cat <<"EOF"
echo "some {}"
EOF
)
arr=("foo" "bar" "baz")
printf "%s\n" "${arr[@]}" \
    | xargs -n 1 \
            -P ${#arr[@]} \
            -I {} \
            bash -c "$script"

Associative Arrays

Using cut

arr=("key1,val1",
     "key2,val2",
     "key3,val3")

for i in "${arr[@]}"; do
    k=$(echo "$i" | cut -d, -f 1)
    v=$(echo "$i" | cut -d, -f 2)
    echo "Key is $k and val is $v"
done

Range

for i in {1..5}; do
    echo $i
done

Strings, Echo, and Printf

Echo adds a newline

Notice that echo adds a newline.

echo "foo" | od -c

While printf does not.

printf "foo" | od -c

Print a multiline string

echo $a prints a heredoc on one line, while echo "$a" preserves the newlines in the heredoc. Why is that?

a=$(cat <<EOF
hello
world
EOF
)
echo $a
echo "---"
echo "$a"

Numbered List

echo $PATH | tr ":" "\n" | nl