codeCompiler77
codeCompiler77

Reputation: 526

Ensure only 2 command line arguments in bash?

CODE: cmd.sh

#!/bin/bash

if [ $# -ne 4 ]; then
    echo not 4
    exit 1
fi

while [[ $# > 1 ]]
do
key="$1"

case $key in
    -f|--file)
    FILE="$2"
    shift
    ;;
    -t|--type)
    TYPE="$2"
    shift
    ;;
    --default)
    DEFAULT=YES
    ;;
    *)
        # unknown option
    ;;
esac
shift
done
echo FILE = "${FILE}"
echo TYPE = "${TYPE}"

GOAL

How do i ensure that the user always has to enter both file and type arguments and ensure that they always enter 2 arguments, not more and not less.

EDIT

Updated the code above, ne 4 because of the flags.

ONLY VALID RESULT

$ ./cmd.sh -f asd -t def
FILE = asd
TYPE = def

CURRENT ISSUE

1.

$ ./cmd.sh -f asd asd asd
FILE = asd
TYPE =

I didn't include the -t flag but it still considers it as valid.

2.

$ ./cmd.sh -f asd -g asd
FILE = asd
TYPE =

I included an invalid flag but it still considers it as valid.

SOLUTION

#!/bin/bash

if [ $# -ne 4 ]; then
    echo not 4
    exit 1
fi

while [[ $# > 1 ]]
do
key="$1"

case $key in
    -f|--file)
    FILE="$2"
    shift
    ;;
    -t|--type)
    TYPE="$2"
    shift
    ;;
    --default)
    DEFAULT=YES
    ;;
    *)
        echo invalid option $key
        exit 1
    ;;
esac
shift
done
echo FILE = "${FILE}"
echo TYPE = "${TYPE}"

LAST ISSUE

How do i ignore the length requirement and check if they type -h or --help? I want to print out the usage in that case.

MY ATTEMPT

#!/bin/bash

# Ensure that environment variables are not used by accident
FILE=
TYPE=
arg0="$(basename "$0" .sh)"

usage() { echo "Usage: $arg0 -f file -t type" >&2; exit 1; }
error() { echo "$arg0: $*" >&2; usage; }
help() { echo "Help: some help file" >&1; exit 0; }
version() { echo "Version: 1.0" >&1; exit 0; }

while [ $# -gt 1 ]
do
    case "$1" in
    -h|--help)
        help
        ;;
    -v|--version)
        version
        ;;
    -f|--file|-t|--type)
        ;;
    -*) error "unrecognized option $1";;
    *)  error "unexpected non-option argument $1";;
    esac
    shift
done

[ $# -eq 4 ] || usage

while [ $# -gt 1 ]
do
    case "$1" in
    -f|--file)
        [ -z "$FILE" ] || error "already specified file name $FILE"
        [ -z "$2" ] && error "empty file name specified after $1"
        FILE="$2"
        shift
        ;;
    -t|--type)
        [ -z "$TYPE" ] || error "already specified file type $TYPE"
        [ -z "$2" ] && error "empty type name specified after $1"
        TYPE="$2"
        shift
        ;;
    -*) error "unrecognized option $1";;
    *)  error "unexpected non-option argument $1";;
    esac
    shift
done

# Should never execute either of these errors
[ -z "$FILE" ] && error "no file name specified"
[ -z "$TYPE" ] && error "no type name specified"

echo FILE = "${FILE}"
echo TYPE = "${TYPE}"

This broke the code and started throwing the following error:

$ bash a2.sh -f typename -t asda
a2: unexpected non-option argument typename
Usage: a2 -f file -t type

The solution below is much more elegant than what i tried to do!

UPDATE - TESTING

$ bash d2.sh -t type -f filename -V
FILE = filename
TYPE = type

$ bash d2.sh -h
d2.sh: no file name specified (and no --default)
Usage: d2 [-d|--default][-h|--help][-V|--version][{-f|--file} file] [{-t|--type} type]

$ bash d2.sh -V
d2.sh: no file name specified (and no --default)
Usage: d2 [-d|--default][-h|--help][-V|--version][{-f|--file} file] [{-t|--type} type]

Upvotes: 1

Views: 911

Answers (2)

Jonathan Leffler
Jonathan Leffler

Reputation: 754450

Does this meet your requirements? I think it covers most of the bases, though if you elect to specify a string of blanks or tabs as a file name or a type name, the script will not object.

Script a2.sh

#!/bin/bash

# Ensure that environment variables are not used by accident
FILE=
TYPE=
arg0="$(basename "$0" .sh)"

usage() { echo "Usage: $arg0 -f file -t type" >&2; exit 1; }
error() { echo "$arg0: $*" >&2; usage; }

[ $# -eq 4 ] || usage

while [ $# -gt 1 ]
do
    case "$1" in
    -f|--file)
        [ -z "$FILE" ] || error "already specified file name $FILE"
        [ -z "$2" ] && error "empty file name specified after $1"
        FILE="$2"
        shift
        ;;
    -t|--type)
        [ -z "$TYPE" ] || error "already specified file type $TYPE"
        [ -z "$2" ] && error "empty type name specified after $1"
        TYPE="$2"
        shift
        ;;
    -*) error "unrecognized option $1";;
    *)  error "unexpected non-option argument $1";;
    esac
    shift
done

# Should never execute either of these errors
[ -z "$FILE" ] && error "no file name specified"
[ -z "$TYPE" ] && error "no type name specified"

echo FILE = "${FILE}"
echo TYPE = "${TYPE}"

Note that if FILE and TYPE were not initialized in the script, running:

FILE=henry TYPE=ford bash a2.sh -f file -t type

would generate errors because FILE would already have a value (or TYPE already has a value with different arguments). Making sure you don't accidentally pick up an environment variable is important. There's also a case for saying that variables in your script should be lower-case (or mixed-case), leaving upper-case for environment variables. However, you'd still need to set lower-case variables — you can just as easily have lower-case environment variables as upper-case, though upper-case is conventional.

Also note the careful, deliberate and correct use of $* in the error function. While using $@ wouldn't cause trouble, this time the desired result is a single string, and $* gives that. (Very often, it is correct to use "$@", but this is a case where "…$*…" is better.)

Example runs

$ bash a2.sh
Usage: a2 -f file -t type
$ bash a2.sh a
Usage: a2 -f file -t type
$ bash a2.sh a b
Usage: a2 -f file -t type
$ bash a2.sh a b c
Usage: a2 -f file -t type
$ bash a2.sh a b c d
a2.sh: unexpected non-option argument a
Usage: a2 -f file -t type
$ bash a2.sh a b c d e
Usage: a2 -f file -t type
$ bash a2.sh -f file -f file
a2: already specified file name file
Usage: a2 -f file -t type
$ bash a2.sh -t type -t typename
a2: already specified file type type
Usage: a2 -f file -t type
$ bash a2.sh -t type t typename
a2: unexpected non-option argument t
Usage: a2 -f file -t type
$ bash a2.sh -t type -k typename
a2: unrecognized option -k
Usage: a2 -f file -t type
$ bash a2.sh -t type -f filename
FILE = filename
TYPE = type
$ bash a2.sh --file firstname -f filename
a2: already specified file name firstname
Usage: a2 -f file -t type
$ bash a2.sh --type typename -t type
a2: already specified file type typename
Usage: a2 -f file -t type
$ bash a2.sh -f '' -t ''
a2: empty file name specified after -f
Usage: a2 -f file -t type
$ bash a2.sh -t '' -f ''
a2: empty type name specified after -t
Usage: a2 -f file -t type
$

Handling --help, --version, --default etc.

See also the comment chain (laziness at work). A revised version of the code might be:

#!/bin/bash

# Ensure that environment variables are not used by accident
FILE=
TYPE=
arg0="$(basename "$0" .sh)"

usemsg="Usage: $arg0 [-d|--default][-h|--help][-V|--version][{-f|--file} file] [{-t|--type} type]"
usage() { echo "$usemsg" >&2; exit 1; }
error() { echo "$0: $*" >&2; usage; }

while [ $# -gt 0 ]
do
    case "$1" in
    -f|--file)
        [ -z "$FILE" ] || error "already specified file name $FILE"
        [ -z "$2" ] && error "empty file name specified after $1"
        FILE="$2"
        shift
        ;;
    -t|--type)
        [ -z "$TYPE" ] || error "already specified file type $TYPE"
        [ -z "$2" ] && error "empty type name specified after $1"
        TYPE="$2"
        shift
        ;;
    -h|--help)
        echo "$usemsg"
        echo ""
        echo "  [-f|--file] filename   Use the specified file name"
        echo "  [-t|--type] typename   Use the specified type name"
        echo "  [-h|--help]            Print this help message and exit"
        echo "  [-V|--version]         Print the version information and exit"
        echo "  [-d|--default]         Use the defaults (default-file and default-type)"
        exit 0
        ;;
    -V|--version)
        echo "$arg0 Version 1.02 (2016-03-18)"
        exit 0
        ;;
    -d|--default)
        [ -z "$FILE" ] && FILE="default-file"
        [ -z "$TYPE" ] && TYPE="default-type"
        ;;
    -*) error "unrecognized option $1";;
    *)  error "unexpected non-option argument $1";;
    esac
    shift
done

[ -z "$FILE" ] && error "no file name specified (and no --default)"
[ -z "$TYPE" ] && error "no type name specified (and no --default)"

echo FILE = "${FILE}"
echo TYPE = "${TYPE}"

I seldom test the number of arguments until after an option processing loop. You should also look at using getopts or the GNU extended version of getopt(1).

Upvotes: 1

OneCricketeer
OneCricketeer

Reputation: 191844

You already are checking if there are any arguments given, so have you tried checking if there are any more or less than 2?

if [ $# -ne 2 ]; then
    # TODO: print usage
    exit 1
fi

 # rest of code here 

Upvotes: 2

Related Questions