Defining functions and local

A function is defined as name() { commands; } and called by its name, like any other command. The parentheses always stay empty; arguments are simply listed after the name when you call it. A function must be defined before it is called, so put functions near the top of the script.

greet() {
    echo "hello, $1"
}

greet water       # hello, water

There is also a function greet { ... } form, but it is bash-only. The form above is more common.

Variables set inside a function are visible to the whole script by default, so a function can silently overwrite a variable of the same name outside it. Declare variables that belong to the function with local.

$ mol=benzene
$ set_mol() { mol=$1; }
$ set_mol toluene; echo "$mol"
toluene
$ mol=benzene
$ set_mol() { local mol=$1; echo "inside: $mol"; }
$ set_mol toluene; echo "outside: $mol"
inside: toluene
outside: benzene

Arguments: $1, $#, "$@"

Functions and scripts receive arguments the same way: a script gets the arguments from the command line, a function gets the ones passed when it is called.

VariableContents
$0script name (still the script name inside a function)
$1, $2, ... ${10}first, second argument. From the tenth on, braces are required
$#number of arguments
"$@"all arguments, each kept as a separate quoted word
"$*"all arguments joined into a single string with spaces

The difference shows when you pass the arguments on to another command. Here a function that only counts its arguments receives a file name containing a space.

$ count_args() { echo "$# args"; }
$ wrap_at()    { count_args "$@"; }
$ wrap_star()  { count_args $*; }
$ wrap_qstar() { count_args "$*"; }
$ wrap_at water.xyz "ethanol conf1.xyz"
2 args
$ wrap_star water.xyz "ethanol conf1.xyz"
3 args
$ wrap_qstar water.xyz "ethanol conf1.xyz"
1 args

Only "$@" passes on the two files correctly. Loop over arguments with for f in "$@" too. Keep "$*" for printing the arguments on one line, in a log message for example.

shift

shift drops $1 and moves the rest down by one. It is useful when the first few arguments are settings and the rest are files.

$ f() { echo "cores: $1"; shift; echo "files: $*"; }
$ f 8 a.xyz b.xyz
cores: 8
files: a.xyz b.xyz

Return codes and output

A bash function can hand back a result in two ways. One is return, which sets the exit status. It only takes integers from 0 to 255 and is meant for success or failure. Without return, the exit status of the last command becomes the function's.

orca_done() {
    grep -q "ORCA TERMINATED NORMALLY" "$1"
}

if orca_done water/water.out; then
    echo "water finished"
fi

The other is to print the result and have the caller capture it with $( ). Strings and decimals such as energies are returned this way.

$ orca_energy() {
>     grep "FINAL SINGLE POINT ENERGY" "$1" | tail -n 1 | awk '{print $5}'
> }
$ e=$(orca_energy water_opt/water_opt.out)
$ echo "E = $e"
E = -76.426369457144

A function used like this must not print anything else to standard output, or it ends up in the variable as well. Send progress messages to standard error with echo "..." >&2.

Options with getopts

To let a script take options like -m r2SCAN-3c -n 8, use getopts. The make_inputs.sh below creates a directory for each xyz file and writes an ORCA input into it. Method, core count and charge can be changed with options.

#!/bin/bash
# create a directory per xyz file and write an ORCA input into it

usage() {
    cat <<EOF
Usage: ${0##*/} [-m method] [-n nprocs] [-c charge] file.xyz ...
  -m  method line (default: r2SCAN-3c)
  -n  number of cores (default: 4)
  -c  total charge (default: 0)
  -h  show this help
EOF
}

method="r2SCAN-3c"
nprocs=4
charge=0

while getopts "m:n:c:h" opt; do
    case $opt in
        m) method=$OPTARG ;;
        n) nprocs=$OPTARG ;;
        c) charge=$OPTARG ;;
        h) usage; exit 0 ;;
        *) usage >&2; exit 1 ;;
    esac
done
shift $((OPTIND - 1))

if [[ $# -eq 0 ]]; then
    echo "error: no xyz files given" >&2
    usage >&2
    exit 1
fi

write_input() {
    local xyz=$1
    local mol=${xyz##*/}
    mol=${mol%.xyz}

    mkdir -p "$mol"
    cat > "$mol/$mol.inp" <<EOF
! ${method} Opt Freq
%pal nprocs ${nprocs} end
%maxcore 3000

* xyz ${charge} 1
$(tail -n +3 "$xyz")
*
EOF
    echo "$mol/$mol.inp"
}

for xyz in "$@"; do
    write_input "$xyz"
done

The first argument to getopts, "m:n:c:h", lists the accepted options. A letter followed by : takes a value, which arrives in $OPTARG. After the options are read, shift $((OPTIND - 1)) discards them and leaves only the file list in "$@".

$ ./make_inputs.sh -m r2SCAN-3c -n 8 *.xyz
ethanol conf1/ethanol conf1.inp
methane/methane.inp
water/water.inp
$ ./make_inputs.sh -m "B3LYP D3BJ def2-SVP" -n 8 water.xyz
water/water.inp
$ head -n 2 water/water.inp
! B3LYP D3BJ def2-SVP Opt Freq
%pal nprocs 8 end

Quote a value that contains spaces. ethanol conf1.xyz, with a space in its name, went through fine because every variable in the script is quoted. Called incorrectly, the script prints its usage and exits with status 1.

$ ./make_inputs.sh -n
./make_inputs.sh: option requires an argument -- n
Usage: make_inputs.sh [-m method] [-n nprocs] [-c charge] file.xyz ...
  -m  method line (default: r2SCAN-3c)
  -n  number of cores (default: 4)
  -c  total charge (default: 0)
  -h  show this help
Usage errors go to standard error

Help requested with -h goes to standard output with exit status 0. Usage printed because of a mistake goes to standard error with >&2 and a non-zero status. That way another script calling this one can tell that it failed.

getopts only handles single-letter options like -n 8. Long options such as --nprocs 8 need a hand-written while/case/shift loop; for research scripts single letters are usually enough.

Loading a function library with source

If several scripts use the same functions, keep them in a separate file and load it with source. source file runs the file line by line in the current shell. The short form is . file.

#!/bin/bash
# orca_utils.sh: functions for other scripts to source

orca_done() {
    grep -q "ORCA TERMINATED NORMALLY" "$1"
}

orca_energy() {
    grep "FINAL SINGLE POINT ENERGY" "$1" | tail -n 1 | awk '{print $5}'
}
#!/bin/bash
# summary.sh: print the last energy or the status of each output file
source "$(dirname "$0")/orca_utils.sh"

for out in "$@"; do
    if orca_done "$out"; then
        printf '%-40s %s\n' "$out" "$(orca_energy "$out")"
    else
        printf '%-40s %s\n' "$out" "not finished"
    fi
done
$ ~/bin/summary.sh jobs/*/*.out
jobs/methane_opt/methane_opt.out         not finished
jobs/water_opt/water_opt.out             -76.426369457144
jobs/water_sp/water_sp.out               -76.321274411145

A plain source orca_utils.sh looks for the file relative to where you run the script from, so it breaks when called from another directory. $(dirname "$0") makes the path relative to the script's own location instead. Sourcing the library from ~/.bashrc makes orca_energy water.out available at the terminal as well.