Shell Memo

I only give some points which are sometime tricky in shell. There are many good shell scripting guides, to which you can refer.

The following recipes are usually in portable shell, when it is specifically for bash it is indicated.

For the numerous differences betwween bash and bourne shell see Bashism - How to make bash scripts work in dash to transform your scripts you can use Autoconf manual - Portable Shell Programming .

Most of the constructs given here for bash work also in the same way for zsh which is very similar for scripting. bash and zsh differences are mainly in the interactive. use It is explained in this linux Magazine article and in zsh wiki.

I summarise the scripting differences in the next paragraph.

Differences between zsh and bash scripts.

  • zsh does not allow ${var/#old/new} and ${var/%old/new} for anchoring the match of old to the start or end of the parameter text, respectively. It is the only difference mentioned in the zsh FAQ.

  • While bash array index begin by 0 those of zsh begin by 1. More details in Compatibility between Zsh and Bash.

  • There are some tricky differences in pattern matching. In both shell with the double [[  ... ]] operator the comparison for ==, !=, =~ is done whith shell patterns.

    The commands:

    $ if [[ "abc" = a*c ]]; then echo true; else echo false;fi
    $ if [[ "abc" == a*c ]]; then echo true; else echo false;fi
    $ [[ "abc" = a*c ]] && echo true || echo false
    $ [[ "abc" = a*d ]] && echo true || echo false
    

    work in the expected way both zsh and bash.

    But in zsh we have:

    $ if [ "abc" == a*c ]; then echo true; else echo false;fi
    zsh: = not found
    $ if [ "abc" = a*c ]; then echo true; else echo false;fi
    zsh: no matches found: a*c
    $ if [ a*c == a*c ]; then echo true; else echo false;fi
    zsh: = not found
    $ if [ a*c = a*c ]; then echo true; else echo false;fi
    zsh: no matches found: a*c
    
    
    
    While in bash:
    
    $ if [ "abc" = a*c ]; then echo true; else echo false;fi
    false
    $ if [ "abc" == a*c ]; then echo true; else echo false;fi
    false
    $ if [ a*c = a*c ]; then echo true; else echo false;fi
    true
    $ if [ a*c == a*c ]; then echo true; else echo false;fi
    true
    

As the single [ does not make pattern matching in bash and the comparison is on the raw strings.

But note that all these last lines are not corrects, as we are using single [ ... ]; in bash the comparison is on strings not patterns; so the right way was to quote the stings and use a single =.

And the right expression

$ if [ "abc" = "a*c" ]; then echo true; else echo false;fi
false
$ if [ "a*c" = "a*c" ]; then echo true; else echo false;fi
true

Is correct in any shell: bash, zsh, or dash.

exit and return

Refs:
Bash Reference Manual: Bourne Shell Builtins

return

return [n] return n or if not supplied the exit status of the last command. In a function it return with the value. When in a script being sourced with the . or source builtin terminate the script with the return value.

Any command associated with the RETURN trap is executed before execution resumes after the function or script.

A return outside of a function or sourced script, will not terminate the script nor return the value, it produces an error. If the shell is running with -e option, it interrupt the script with an error code.

exit

exit [n]: Exit the shell, returning a status of n to the shell’s parent. If n is omitted, the exit status is that of the last command executed. Any trap on EXIT is executed before the shell terminates.

Reading a query string

To define variable a and b with respective values from the QUERY a=1234&b=9876

A very simple code that should be avoided because the eval here are uncontrolled is

IFS="&" set -- $QUERY
for l in $@; do
    eval $l
done

Read BASH FAQ: Eval command and security issues for an explanation of the danger, and how to avoid it.

A better solution is to use indirect variables or associative arrays, their use is explained in the BASH FAQ: How can I use variable variables or associative arrays?

In bash 4+ we can define an associative array var with each value:

declare -A var
IFS="&" set -- $QUERY
for i in $@; do
    IFS="=" set -- $i
    var[$1]=$2
done

Chris F.A Johnson gave a more complete function that does uudecode in Parsing Web Form Input in CGI Shell Scripts (also available in Dr. Dobbs )

parse_query() #@ USAGE: parse_query var ...
{
    local var val
    local IFS='&'
    vars="&$*&"
    [ "$REQUEST_METHOD" = "POST" ] && read QUERY_STRING
    set -f
    for item in $QUERY_STRING
    do
      var=${item%%=*}
      val=${item#*=}
      val=${val//+/ }
      case $vars in
          *"&$var&"* )
              case $val in
                  *%[0-9a-fA-F][0-9a-fA-F]*)
                       printf -v val "%b" "${val//\%/\\x}"
                       ;;
              esac
              ;;
      esac
    done
    set +f
}

You find a more detailled solution using associative bash arrays; and another with indirect variables in Bash FAQ: How do I write a CGI script that accepts parameters?

capturing input

The tee command allow to duplicate stdout.

The script command can be used to capture the input and output to/from an application.

To replay a session you want to capture only the input. I achieve it by using:

$ cat /dev/stdin|tee /tmp/session_input| application

command return status

  • You can test the status of a command by executing it in a conditional block:

       $ if echo "foo"; then echo "ok"; fi
       foo
       ok
    
    If you want to keep this status you get it in the ``$?`` variable.
    You can later on test it against the ``0`` value that mean *true* for
    the shell.
    
    ..  code-block:: shell-session
    
        $ echo "foo"; if [ $? -eq 0 ]; then echo "ok"; fi
        foo
        ok
    
  • but this convention is the opposite of C where 0 means false, so if you want in a shell supporting numerical expressions in ((...)) to use the numerical testing, which is C compatible you have to negate it.

    $  echo "foo"; if ! (($?)); then echo "ok"; fi
    foo
    ok
    
  • avoid this: $? is a number not a test so you cannot put it directly in an if expression.

  • avoid this: [ 1 ] is true so a test like [ $? ] fail when $? is not defined, but succeed with both 0 and 1 values.

  • an alternative is to use a number comparison:

    if [ $? -eq 0 ]; then echo "ok"; fi
    

Command Expansion.

The Bash Reference gives a detailled description of each part of shell expansion, wich takes place during simple command expansion.

The order of expansion is very important; it is: brace expansion; tilde expansion, parameter and variable expansion, arithmetic expansion, command substitution from left-to-right, word splitting, filename expansion, process substitution, quote removal.

To quote the manual: Only brace expansion, word splitting, and filename expansion can change the number of words of the expansion; other expansions expand a single word to a single word.

Parameter and Command Substitution

Parameter expansion commes before command substitution, it explains why we have:

$ a=foo; a=bar echo $a
foo
$ a=foo; a=bar /bin/echo $a
foo
$ a=foo; a=bar; echo $a
bar
$ a=foo; a=bar; /bin/echo $a
bar

In the second assignement the parameter substitution is done before the assignment.

This is true for a builtin like echo or a command like /bin/echo

You can find more on this subject in Bruce Barnett Grymoire - sh, a subtle point.

Filename expansion

After word splitting comes filename expansion where all the patterns found in the filename are expanded.

The following are done with default shopt settings which disable dotglob and nullglob.

$ mkdir dir
$ cd dir
$ touch b .b a aa aaa .a .aa ..a ba ab bab
$ echo a*
a  aa  aaa  ab
$ echo *a
a aa aaa ba
$ echo a?
aa ab
$ echo ?a
aa ba
$ echo ??
aa ab ba
$ echo *
a aa aaa ab b ba bab
$ echo .*
. .. .a ..a .aa .b
$ echo * .*
a aa aaa ab b ba bab . .. .a ..a .aa .b
$ echo .?*
.. .a ..a .aa .b
$ echo .??*
..a .aa

As we see there is no easy way to catch all files from a directory, the default does not match dotfiles, and if we add the initial dot we catch too less or too much. If we use bash you we can set an option dotglob so pattern can cath the dot, let try again:

$ shopt -s dotglob
$ echo *a
a  .a  ..a  aa  .aa  aaa  ba
$ echo a?
aa  ab
$ echo ?a
.a  aa  ba
$ echo ??
.a  aa  ab  .b  ba
$ echo *
a .a ..a aa .aa aaa ab b .b ba bab
$ echo .*
. .. .a ..a .aa .b
$ echo .?*
.. .a ..a .aa .b
$ echo .??*
..a .aa

It is now easy to catch all files from the directory, but as we see for the last three commands none of these patterns expand to the dotfiles, the first catch also the directory itself and the parent directory, the next one catch also the parent directory, and the last one miss some filenames. Making an operation only on dotfiles is quite common so we must find a way to match all of them, and only them.

In bash we have an environment variable GLOBIGNORE which is not set by default, f GLOBIGNORE is set, each matching file name that also matches one of the patterns in GLOBIGNORE is removed from the list of matches. After setting globignore by GLOBIGNORE=.:.. we get

$ echo .*
.a ..a .aa .b

which is the proper list of dotfiles.

A special feature is that setting GLOBIGNORE to a non null value always disable matching . and .. so we have had the same result by setting GLOBIGNORE=qwertyuop !

Note that if we want to change GLOBIGNORE only for some expansions we ca do it in a subshell.

$ (GLOBIGNORE=.:..; echo .*)
.a ..a .aa .b

but as explained above, the following does not work :

$ GLOBIGNORE=.:.. echo .*
. .. .a ..a .aa .b

The previous use of shopt is only possible in bash, these options don’t exist in plain bourne shell. But we can always match all dotfiles with

$ echo .[!.]* ..?*
.a .aa .b ..a
$ echo .[^.] .??*
.a .b ..a .aa

When a range match begins with ! or a ^ any character not enclosed is matched, so with these two patterns we get rid of the unwanted . and ...

Parameter Expansion, Splitting and Quote Removal.

As parameter expansion is done before word splitting, and quote removal after word splitting, we have:

$ IFS=: set -- a:b:c; echo $1
a b c
$ string="a:b:c"
$ IFS=: set -- $string; echo $1
a
$ IFS=: set -- "$string"; echo $1
a b c

But when we have:

$ IFS=: read x y z <<< "a:b:c"; echo $x
a
$ string="a:b:c"
$ IFS=: read x y z <<< $string; echo $x
a
$ IFS=: read x y z <<< "$string"; echo $x
a
$ echo $string | { IFS=: read x y z; echo $x; }
a

In these four the input file descriptor is built before the read, and the three input are the same.

Quoting and splitting.

The bash reference describe Quoting but sometime the combination of quoting with Command Expansion can be difficult to sort out.

If you set a variable to a string, when use it as parameter in the process of Simple Command Expansion it is subject to Shell Expansion a complex process which involves Shell Parameter Expansion, Command Substitution, Arithmetic Expansion, Process Substitution, and then Word Splitting followed by Filename Expansion and Quote removal.

Let’s apply with a simple command, made from a simple script named countargs:

#! /bin/sh
echo nbargs: $#
i=0
for a in "$@"; do
    i=$((i+1)) # ++i in bash!
    echo $i ':' "$a"
done
$ x="one two three"
$ ./countargs $x
nbargs: 3
...
$ ./countargs "$x"
nbargs: 1
1 : one two three

Very simple indeed, this is the expected behavior of quotting and word splitting.

But if you use your parameter in assignment the rules are different, assignment are not commands but a preliminary to Simple Command Expansion and quoting this section

There is no word splitting there, so the last two assignments are valids and equivalents:

$ x="one two three"
$ y=$x
$ z="$x"
$ echo $y
one two three
$ echo $z
one two three

Now if you use the special characters used for glob patterns like ? and * in a word assigned to a variable, and evaluate the variable afterward the Filename Expansion can occur so you get:

$ x=sound*
$ echo $x
sound.wav
$ echo "$x"
sound*
$ y=$x
$ echo "$y"
sound*
$ y=$(echo $x)
$ echo "$y"
sound.wav
$ y="$(echo $x)"
$ echo "$y"
sound.wav

Note that in the first assignement we have not used quotes, because as set previously in the text after an assignement the is no Filename expansion but of course there is quote removal, so the three following assignements are identical:

$ x='sound*'
$ x="sound*"
$ x='sound*'

In the same way the line breaks in a string assigned to a variable are used for word splitting so:

$ x="
> one
> two
> three"
5231$ echo $x
one two three
5232$ echo "$x"

one
two
three
$ y=$x
$ echo "$y"

one
two
three

As usual a trailing backslach remove the following newline:

$ x="one \
two \
three"
$ echo "$x"
one two three

The other backslash escape are uninterpreted, but you can force the interpretation with the command echo with the -e argument or the command printf. Both commands exists as Bash builtins. As printf is not a dash builtin, and the dash echo builtin does not support the -e argument; in portable shell you better rely on the command printf or the command echo.

$ x="one\ntwo\nthree"
$ echo $x
one\ntwo\nthree
$ echo -e $x
one
two
three
$ printf "%b\n" $x
one
two
three

file descriptors

Reference

Opening - Closing - Listing

To assign fd 3 to myfile:

$ exec 3>myfile

To close fd 3:

$ exec >&3-

For input descriptors:

$ exec 3<myfile
$ exec <&3-

To open a fd for read-write:

$ exec 3<>myfile

To list open file descriptors:

$ ls -l /dev/fd/*

or:

$ lsof -a -p $$ -d 0-10

Copying - Moving

To copy a file descriptor you can use:

1
2
3
4
5
$ exec 3>&1
$ exec 1>|/tmp/output1
$ ls
$ exec 1>&3
$ exec 3>&-

In line (1) the file descriptor 1 is copied to fd 3, then (2) 1 is redirected to the file /tmp/output1, (3) the first ls goes in this file, then (4) fd 3 is copied back to fd 1 which comes back to it’s previous value; (5) the descriptor 3 is then closed.

In Bash but not dash the last two lines can be abbreviated in:

$ exec 1>&-3

Swapping stdout and stderr.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ f(){ echo out; echo error >&2; }
$ f
out
error
$ x=$(f)
error
$ echo $x
out
$ x=$(f 2>&1)
$ echo $x
out error
$ x=$(f 1>&2)
out
error
$ echo $x

$ x=$(f 3>&2 2>&1 1>&3)
out
$ echo $x
error
$ exec 3>&2; x=$(f 2>&1 1>&3); 3>&-
out
$ echo $x
error
$ exec 3>&1; x=$(f 2>&1 1>&3); 3>&-
out
$ echo $x
error
(2)
Normal call to f, both error and output go to standard output.
(5)
The output is assigned to x and the error go to stdout.
(9)
The output fd replace the error, so both fd go to stdout and are assigned to x.
(12)
The error fd replace the output, so both echos are going to error and nothing on stdout, x is empty.
(17)
We execute f in a context where error is saved as fd 3, then output replacing error, previous saved error replace output.
(21)
is similar to (17) except that we save stderr before evaluating the expression.
(25)

Is harder to understand, and can seem paradoxal first, what use of saving fd (file descriptor) 1 for copying it later to the same fd 1?

But the standard output outside and inside of the Command Substitution are not the same. Before the Command Substitution the current output is untouched, if we execute this script from a pseudo terminal it is /dev/pts/0 and this value is saved to fd 3.

Inside $(...) which is a Command Substitution construct, the standard output is redirected to a pipe to capture the output; we copy this pipe to fd 2 with 2>&1, then the fd 3 containing the original output is copied back to the current output that is fd 1.

We execute the function in this context and affect the content of the pipe, that is what goes to stderr inside the function, to the variable; then fd 3 is closed.

Reading from a bloc of text.

The shell builtin read first aim is reading from standard input or any file descriptor. But we can use it with Here Documents, or with Here Strings in bash or zsh but not dash, it is not a portable construct, in a pipe, or with process substitution.

In standard shell like dash when we read from an Here Documents the builtin read read one line each time. In bash or zsh we can change the delimiter and read the whole Here Document or Here String in a single shell variable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ f=foo
$ read -d'' x <<EOF
$f and bar
        go in a boat
EOF
$ echo "$x"
foo and bar
        go in a boat
$ read -d'' x << "EOF"
$f and bar
        go in a boat
EOF
$ echo "$x"
$f and bar
    go in a boat
$ read -d'' x <<- EOF
$f and bar
        go in a boat
EOF
$ echo "$x"
foo and bar
go in a boat

As seen in the previous example (9) quoting the end string disable variable expansion, and (16) using <<- delete initial tabulations.

Extracting the parts of a string.

Very often we want to extract fiels from a strings separated with a single character. It may be a space or an other character, we often choose ,, :, ;, !, |, %, / or \ but any char can be choosen as far it is not member of the strings; if we want to be free from this limitation we have to add an escaping mechanism which is not dealed here.

A related problem is when the fields are separated by any sace sequence constituted by space characters and tab characters, but it is easily converted to the previous case with one of:

string=$(echo $string0 | sed 's/[[:space:]]\{1,\}/ /g')
string=$(echo $string0 | sed -r 's/[[:space:]]{1,}/ /g')

using either old basic regex or standard posix 2 extended regex. But when you use the shell primitives you don’t need this step as the shell with the default IFS use sequence of spaces to delimit words.

For the example we suppose the fields are delimited by :.

We can of course use basic coreutils

string="a:123:456"
v1=$(echo $string | cut -d: -f1)
v2=$(echo $string | cut -d: -f2)
v3=$(echo $string | cut -d: -f3)

But we can use only the shell even basic bourne shell or dash.

string="a:123:456"
IFS=":" set -- $string
v1=$1
v2=$2
v3=$3

Or using Here Documents that is found in any shell:

string="a:123:456"
IFS=: read v1 v2 v3 <<EOF
$string
EOF

We can also use in dash, busybox ash, yash, bash, zsh and others POSIX compatibles shells Shell Parameter expansion

string="a:123:456"
var="${string}::"
i=0
while [ "$var" != ':' ]; do
  i=$((i+1))
  # drop part of string from first ':' to the end
  iter=${var%%:*}
  echo "v_$i=$iter"
  # drop begin of string upto first ':'
  var="${var#*:}"
done

Here we just echo the variables name we don’t set them. To set the variables v_1, v_2, v_3, in bash or zsh we can replace the echo by:

declare v_$i="$iter"

or use the POSIX directive typeset that is synonim to declare in bash and zsh; but is also available in yash:

typeset v_$i="$iter"

in any bourne shell, dash or ash we fallback to an eval.

eval v_$i="$iter"

But quite harmless as we know the range of values of $i.

In bash, yash or zsh we can also use Here String

string="a:123:456"
IFS=: read v1 v2 v3 <<< "$string"

Also bash yash and zsh can use arrays to read the values; it is peculiarly useful when you don’t know how many fields can be present.

In bash you write:

$ string="a:123:456"
$ IFS=: read -a v <<< "$string"
$ echo "${v[@]}"
a 123 456
$ declare -p v
declare -a v=([0]="a" [1]="123" [2]="456")

You can also use the previous code in yash or zsh; with a slightly different syntax:

$ string="a:123:456"
$ IFS=: read -A v <<< "$string"
$ echo "${v[@]}"
a 123 456
$ typeset -p v
v=('a' '123' '456')

An other way in bash or zsh is to assign directly the array.

$ string="a:123:456"
$ IFS=: v=(${string})
$ declare -p v
declare -a v=([0]="a" [1]="123" [2]="456")

As an example of use we parse the ouput of the route command to find the different fields of the default route.

read dest gateway mask flags metric ref use iface <<EOF
$(route -n | grep '^0\.0\.0\.0' )
EOF

But when we use the previous methods to parse the output of a command; the command should be executed before the parsing; in some case e might want to prefer, or be obliged, to process asynchronously the results. Unix use pipes for this, and everything works as long as you want to use the result of the previous process in the subsequent one. But it can be more difficult to get the result of a subprocess inthe parent process. This is the subject of next paragraph.

Using an asynchronous subprocess.

A pipe open a subshell. As variables in a subshell are inaccessible from the parent shell, all the variables set inside a pipe are also unavailable out of the pipe.

We often meet this problem while trying to read from the output of a pipe:

$ x="unset"
$ y="unset"
$ echo one two | { read x y; echo $x $y; }
one two
$ echo $x $y
unset unset

We can use redirection to avoid a subshell, we can either use a temporary file, a fifo or process substitution.

The use of a temporary file is allowed in bare bourne shell, dash, ash, yash and usefull for portable script.

$ echo "a b">|/tmp/tmpfile
$ exec 4< /tmp/tmpfile
$ read x y <&4
$ echo $x $y
a b
$ exec 4<&-
$ rm /tmp/tmpfile

If we need to split our string on another character than a sequence of spaces, tabs and newlines, which constitute the default IFS, we only change the IFS before the script and reset it or change it into a subprocess.

But here changing IFS for only the read works as well.

$ echo "a:b">|/tmp/tmpfile
$ exec 4< /tmp/tmpfile
$ IFS=":" read x y <&4
$ echo $x $y
a b
$ exec 4<&-
$ rm /tmp/tmpfile

If our shell and system admit process substitution, which is the case of bash, and zsh on systems that support named pipes (FIFOs) or the /dev/fd files:

$ exec 4< <(echo a b)
$ read x y <&4
$ echo $x $y
a b
$ exec 4<&-

Or simply, a more condensed form:

$ read x y < <(echo a b)
$ echo $x $y
a b

This can even be used in yash with process redirection whose syntax is different from process substitution which is found in bash or zsh.

$ read x y <(echo a b)
$ echo $x $y
a b

As these custom syntaxes are not in Posix; and for portability it is better to avoid them.

A pipe create an implicit fifo, which imply to use a subshell, but we can also avoid it and keep the benefit of forking a producer by using an explicit fifo. This is also available in any shell.

$ mkfifo /tmp/fifo
$ echo a b >/tmp/fifo &
[1] 6934
$ read x y </tmp/fifo
$ echo $x $y
a b
[1]+  Done    echo a b > /tmp/fifo
$ rm /tmp/fifo

We illustrate the use of process substitution to parse the ouput of the route command to find the different fields of the default route; that we did previously <parsing-route-1> sequentially.

read dest gateway mask flags metric ref use iface < \
<(route -n | grep '^0\.0\.0\.0' )

If we don’t need the asynchronous processing of the previous scripts we can store the output of the first command in a string and read from that string this is illustrated by the paragraph on splitting output of a command.

Elapsed time of a command

To get the time of a command we can use the time command

$ /usr/bin/time -f "%e elapsed, %U user, %S sys" locate xzuv
Command exited with non-zero status 1
1.72 elapsed, 1.61 user, 0.04 sys

We can also under bash use the internal time bash command, this one can be used not only with a command but before any pipe, command group, or subshell

$ time locate xzuv
$ time (ls >/dev/null; cat /etc/passwd >/dev/null)
$ time { ls >/dev/null; cat /etc/passwd >/dev/null; }
real    0m0.021s
user    0m0.000s
sys     0m0.012s

To know the elaped time of some part of a script we can also use the date command:

before="$(date +%s)"
..... #some shell commands
after="$(date +%s)"
echo "elapsed: $(date -u -d @$(($after - $before)) +%H:%M:%S)"

array indexes and globbing.

Bash can store arrays in a shell variable, but interaction between array indexes and pathname expansion is a tricky and badly documented aspect of bash.

This is summarized by the next small script

$ echo t[1]
t[1]
$ shopt -s nullglob
$ echo t[1]

$ touch t1
$ echo t[1]
t1

The same pathname expansion is done after an unset command, so doing unset t[1] may result in unsetting the array element t[1] or the unsetting the variable t1 or causing an error or doing nothing, depending on the presence of a file named t1 and of the setting of the options nullglob, failglob, extglob, and the environment variable GLOBIGNORE

So you are advised always quoting the argument of an unset and write: unset 't[1]'.

Note that within an expression like ${t[1]} braces disable pathname expansion

bash and zsh regex expressions.

In bash, since version 3.0, you can match gnu regex, it allows to dispense with the call to expr or sed (the price is a lesser compatibility with older release of batch or other bourne shells, it is definitely not posix).

You use it like this:

$ [[ "abbbaaaaabbb" =~ a*(b*)(a*)(ab*) ]]
$ echo "${BASH_REMATCH[@]}"
abbbaaaaabbb bbb aaaa abbb

The array variable BASH_REMATCH contains substrings matched by parenthesized subexpressions.

In zsh if the option BASH_REMATCH is set the result is identical to the bash one. Otherwise we get the global match with $MATCH; and the groups with ${match[@]}

$ [[ "abbbaaaaabbb" =~ a*(b*)(a*)(ab*) ]]
$ echo "$MATCH"
abbbaaaaabbb
$necho "${match[@]}"
bbb aaaa abbb

The match in bash and the default on zsh is using POSIX regex functions, the same we use in grep.

Zsh can also test the regexp as a PCRE regular expression by setting the option RE_MATCH_PCRE.

Using getopts.

getopts is a POSIX function; defined in bash and zsh.

In bash the details are reviewed in abs: example 11-8, and you get a summary by typing help getops under the shell.

In zsh $ getopts followed by Esc-h give the interactive help.

When using it in a function it looks like that:

local opt OPTARG
local -i OPTIND=1
while getopts :d:D:p:F opt; do
    case $opt in
        d|D) myoptarg1=$OPTARG ;;
        p) myoptarg2=$OPTARG ;;
        F) myopt3=true ;;
        *) help $FUNCNAME
        exit 2
    esac
done
shift $(( OPTIND - 1 ))

If not in function replace local by declare

using getopt.

An example is given with getopt(1) in /usr/share/doc/util-linux/examples/, it is recalled here:

# We need TEMP as the `eval set --' would nuke the return value of getopt.
TEMP=$(getopt -o ab:c:: --long a-long,b-long:,c-long:: \
     -n 'example.bash' -- "$@")
if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
# Note the quotes around `$TEMP': they are essential!
eval set -- "$TEMP"
while true ; do
   case "$1" in
       -a|--a-long) echo "Option a" ; shift ;;
       -b|--b-long) echo "Option b, argument \`$2'" ; shift 2 ;;
       -c|--c-long)
            # c has an optional argument. As we are in quoted mode,
            # an empty parameter will be generated if its optional
            # argument is not found.
            case "$2" in
                "") echo "Option c, no argument"; shift 2 ;;
                 *)  echo "Option c, argument \`$2'" ; shift 2 ;;
            esac ;;
       --) shift ; break ;;
       *) echo "Internal error!" ; exit 1 ;;
   esac
done
echo "Remaining arguments:"
for arg do echo '--> '"\`$arg'" ; done

getopt and whitespaces.

he old version of getopt does preserve whitespaces in arguments so you

get:

$ getopt a: -- -a "one two" "three four"
 -- -a one two three four

This stand either with the old getopt or the enhanced one, as the latter generate output that is compatible with that of other versions, as long as his first parameter is not an option.

This defect is the cause of getopt rejection in some manuals as in the SHELLdorado good coding practices

But if you use the enhanced version with it’s new syntax you get:

$ getopt --options a: -- -a "one two" "three four"
 -a 'one two' -- 'three four'

So the whitespaces are preserved, with the new getopt;. You can check that yourgetopt; is the enhanced one by doing getopt -V or in a script:

$ getopt -T
$ if [ $? -eq 4 ]; then
# code for new getopt

The return value of 4 is the sign of the enhanced version.