2

I want to detect if either no arguments or an invalid argument is passed and print a help message. A separate check for an empty argument is possible, but not so elegant.

My bash script looks like this:

COMMAND="$1"
shift
case "$COMMAND" in     
        loop)
            loop_
            ;;  
        ...            
        *)
            echo $"Usage: $0 {loop|...}"
            exit 1 
esac

When no arguments are passed, nothing executes; if I pass "" then the proper case is triggered. If I use $1 directly instead of using the temporary variable, then it works as expected.

I've even tried adding a specific case for "") but to no avail.

codeforester
  • 39,467
  • 16
  • 112
  • 140
Reinstate Monica
  • 588
  • 7
  • 21
  • Possible duplicate of [Check existence of input argument in a Bash shell script](https://stackoverflow.com/questions/6482377/check-existence-of-input-argument-in-a-bash-shell-script) – PesaThe Jul 29 '18 at 10:07
  • @PesaThe I need the default case anyway, to catch an invalid command. I would like to include the "no command" case in the switch and not as a separate check. – Reinstate Monica Jul 29 '18 at 10:53
  • 3
    ...anyhow, `*)` **does** match the empty case. If you want to claim that it doesn't, you'll need to provide a [mcve] letting someone else see the issue for themselves. See https://ideone.com/rGIvg4 showing your code -- modified only to add the missing `;;` -- properly emitting a usage error when no command is given. – Charles Duffy Jul 29 '18 at 21:19

5 Answers5

8

The only way your case statement isn't going to match with no $1 given is if it isn't entered in the first place.

Consider the following:

#!/usr/bin/env bash
set -e

command=$1
shift
case $command in
  *) echo "Default case was entered";;
esac

This emits no output when $1 is unset -- but not because anything wrong with the case statement.

Rather, the issue is that shift exits with a nonzero exit status when there's nothing available to shift, and the set -e causes the script as a whole to exit on that failure.


First Moral Of This Story: Don't Use set -e (or #!/bin/bash -e)

See BashFAQ #105 for an extended discussion -- or the exercises included therein if in a hurry. set -e is wildly incompatible between different "POSIX-compliant" shells, and thus makes behavior hard to predict. Manual error handling may not be fun, but it's much more reliable.


Second: Consider A Usage Function

This gives you a terse way to have your usage message in one place, and re-use it where necessary (for example, if you don't have a $1 to shift):

#!/usr/bin/env bash

usage() { echo "Usage: $0 {loop|...}" >&2; exit 1; }

command=$1
shift || usage
case $command in
  *) usage ;;
esac

Because of the || usage, the exit status of shift is considered "checked", so even if you do run your script with set -e, it will no longer constitute a fatal error.


Alternately, Mark The shift As Checked Explicitly

Similarly:

shift ||:

...will run shift, but then fall back to running : (a synonym for true, which historically/conventionally implies placeholder use) should shift fail, similarly preventing set -e from triggering.


Aside: Use Lower-Case Names For Your Own Variables

POSIX specifies that the shell (and other tools to which the standards applies) have their behavior modified only by environment variables with all-caps names:

Environment variable names used by the utilities in the Shell and Utilities volume of POSIX.1-2017 consist solely of uppercase letters, digits, and the ( '_' ) from the characters defined in Portable Character Set and do not begin with a digit. Other characters may be permitted by an implementation; applications shall tolerate the presence of such names. Uppercase and lowercase letters shall retain their unique identities and shall not be folded together. The name space of environment variable names containing lowercase letters is reserved for applications. Applications can define any environment variables with names from this name space without modifying the behavior of the standard utilities.

This applies even to regular, non-exported shell variables because specifying a shell variable with the same name as an environment variable overwrites the latter.

BASH_COMMAND, for example, has a distinct meaning in bash -- and thus can be set to a non-empty value at the front of your script. There's nothing stopping COMMAND from similarly being meaningful to, and already used by, a POSIX-compliant shell interpreter.

If you want to avoid side effects from cases where your shell has set a built-in variable with a name your script uses, or where your script accidentally overwrites a variable meaningful to the shell, stick to lowercase or mixed-case names when writing scripts for POSIX-compliant shells.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
3

The easiest way to solve your problem without altering your general pattern is to use “advanced” parameter expansion features from the Bourne shell (this is not bash-specific). In this case, we can use the :- modifier to supply default values:

COMMAND="${1:-triggerusagemessage}"
shift
case "$COMMAND" in     
        loop)
            loop_
            ;;  
        ...            
        triggerusagemessage)
            echo $"Usage: $0 {loop|...}"
            exit 64
            ;;
esac

See the paragraph “Parameter Expansion” in the man page of your shell for a short presentation of the available parameter expansion modifiers.

(Note the exit code 64, which is reserved for this case on some operating systems.)

Michaël Le Barbier
  • 6,103
  • 5
  • 28
  • 57
  • 1
    The pattern you suggest here fixes support for `set -u`, but it doesn't address compatibility with `set -e`; the `shift` still fails when no arguments exist. And if the OP were using `set -u`, they'd get an explicit error message, not a silent failure, so they presumably wouldn't have asked a question assuming there was something wrong with the `case` statement. – Charles Duffy Jul 29 '18 at 22:11
  • Out of curiosity -- on which operating systems is exit code 64 reserved for usage errors? – Charles Duffy Jul 29 '18 at 22:13
  • @CharlesDuffy I did not want to alter much the pattern used by the OP, however I prefer and recommend to separate argument analysis from the code… (So argument analysis only prepares variables and finds out which function to actually call.) But the OP rules out this approach “A separate check for an empty argument is possible, but not so elegant.” FreeBSD and possibly other BSDs have a [sysexits(3)](https://www.freebsd.org/cgi/man.cgi?query=sysexits&apropos=0&sektion=0&manpath=FreeBSD+11.2-stable&arch=default&format=html) manpage recommending using exit code 64 for usage error. – Michaël Le Barbier Jul 29 '18 at 22:31
  • Thank you for that reference. Backing up, though -- I'm still not convinced that this answer *does* in fact solve the OP's issue; to my reading, it appears to rely on an assumption that the diagnosis (implicit in the question) that `*)` doesn't match the empty string was correct. If that wasn't the problem, then replacing your empty string with a constant doesn't solve it. – Charles Duffy Jul 29 '18 at 22:51
0

You can simply use $#. It represents the number of given arguments:

if [ $# = 0 ] 
then
  echo "help ..."
fi  
Rene Knop
  • 1,788
  • 3
  • 15
  • 27
  • I would like to retain the `*)` case for when an invalid command is given, and I don't wan't to repeat the help message... [I could use a function etc..]. I hope there's a cleaner way. – Reinstate Monica Jul 29 '18 at 10:52
0
#!/bin/bash
################################################################################
# Running this script with:                                                    #
#   * No option displays the specified message.                                #
#   * An invalid option displays the specified message.                       #
#   * A valid option displays the specified message.                           #
#   * An option with an argument displays the specified message.               #
#   * Multiple valid options display the specified messages for each.          #
#   * Arguments provided for options that don't require them will be ignored.  #
################################################################################
# Notes about getopts and how it handles arguments:                            #
#   * A colon before the options allows custom messages for missing arguments. #
#   * A colon after an option indicates that it requires an argument.          #
################################################################################
# In this example script:                                                      #
#   * Custom messages for missing arguments are allowed.                       #
#   * Options -a and -b and -c and -h are valid.                               #
#   * All other options are invalid.                                           #
#   * Option -a does not require an argument.                                  #
#   * Option -b requires an argument.                                          #
#   * Option -c requires an argument.                                          #
#   * Option -h does not require an argument.                                  #
################################################################################

case "$1" in
    # Valid options:
    -a | -b | -c | -h)
        # Loop through valid options, handling arguments:
        while getopts :ab:c:h opt;
            do
                case $opt in
                    # Valid option with missing argument:
                    :) echo "You must use an argument with the -$OPTARG option.";;
                    # Valid option:
                    a) echo "You have used the -$opt option.";;
                    # Valid option with argument:
                    b) echo "You have used the -$opt option with the $OPTARG argument.";;
                    # Valid option with argument:
                    c) echo "You have used the -$opt option with the $OPTARG argument.";;
                    # Valid option:
                    h) echo "You have used the -$opt option to display the help text.";;
                esac;
            done;
        ;;
    # Missing options:
    "") echo "You have not used an option.";;
    # Invalid options:
    *) echo "You have used the invalid $1 option.";;
esac

Little Girl
  • 168
  • 6
-2

in the line: echo $"Usage: $0 {loop|...}" what's the first $ for?

If you don't want to repeat the message, just put it in a function and check for an empty string before the case statement.

#! /bin/bash

die()
{
        "Usage: $0 {loop|...}"
        exit 1
}

COMMAND="$1"
[ -z $COMMAND ] && die 
shift
case "$COMMAND" in
        loop)
            loop_
            ;;
        *)
            die 
            exit 1
            ;;
esac
Tom Zych
  • 13,329
  • 9
  • 36
  • 53
  • 1
    `$"..."` looks up the string in the active translation table; it's a feature for locale/multilingual support in scripts. – Charles Duffy Jul 29 '18 at 21:14
  • 1
    And your code tries to run `Usage:` as a command, not echo it, so it throws an error. – Charles Duffy Jul 29 '18 at 21:14
  • 1
    Also, `[ -z $foo ]` is broken. If `$foo` is empty, it becomes `[ -z ]`, which is equivalent to `[ -n '-z' ]`. If `foo='1 -o 1 = 1'`, it becomes `[ -z 1 -o 1 = 1 ]`, the string *isn't* empty, but you still get a true result. Always use quotes: `[ -z "$foo" ]` – Charles Duffy Jul 29 '18 at 21:18