1

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]
codeCompiler77
  • 508
  • 7
  • 22
  • You already have a condition for `$# > 1`, so do you not understand what that means? – OneCricketeer Mar 19 '16 at 03:28
  • Your code indicates that you want `yourscript --file filename -t typename`, which has 4 arguments, to be accepted (so your description of '2 arguments' is misleading). It isn't clear what should happen with `yourscript --default`. Presumably, you want to prevent `yourscript --file file1 -f file2` from being acceptable (because there's no type information), and similarly for other repeated options. Is that accurate? – Jonathan Leffler Mar 19 '16 at 03:56

2 Answers2

2

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 
OneCricketeer
  • 179,855
  • 19
  • 132
  • 245
1

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).

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • That is so much nicer and cleaner than what i have written! Thank you for the explanation as well :) If the user enters `-h` or `--help`. Just argument and not more, would i perform this check above the `[ $# -eq 4 ] || usage`? – codeCompiler77 Mar 19 '16 at 05:27
  • Yes, if you want just `-h` or `--help` to produce help, you have to test that before you reject the wrong number of arguments. However, consider what should happen if the user runs (in my notation) `bash a2.sh -f file -h help`; you probably want to detect that and report the help again. Use a function to do the job — you can use it in both places. Similarly, you could add a test for `-d` or `--default` and use `set -- -f default-file -t default-type` to set the default values; again, you'd need to consider what to do with that appearing in `bash a2.sh --type type-name --default --help`, etc. – Jonathan Leffler Mar 19 '16 at 05:31
  • The other way of dealing with these is to defer the 'number of arguments' test until later. Deal with `--help` and `--default` (and `--version`, for example) in the loop. After the loop, you can check that both `FILE` and `TYPE` are non-empty. You already know there are no odd-ball arguments left over because the loop doesn't exit until there are no arguments left (or a mistake has been made). – Jonathan Leffler Mar 19 '16 at 05:34
  • is there a guideline on what to do in that this situation? `bash a2.sh --type type-name --default --help` or a best practice when it comes to bash? – codeCompiler77 Mar 19 '16 at 05:34
  • Each case has to be considered on its merits. Allow `--help` anywhere and report the help and exit when it is received. Similarly with `--version`. The `--default` is less clear-cut; it isn't as common as all that. If you have defaults, you would use them and wouldn't insist on the user providing both type and file information (they'd be options, not requirements). If you do have `--default`, does it make sense to allow it after `--type` or `--file`? What if the user uses `bash a2.sh --default -t type -f file`, rendering the defaults moot? Generally, I'd allow that — it does no harm. – Jonathan Leffler Mar 19 '16 at 05:38
  • Fair point, by `default`. Just for my clarification, you are referring to default values so that the user doesn't have to provide input. In the event that they did something like this then `bash a2.sh --default -t type`. Type would be overwritten but the default filename would be unchanged? Also, in regards to `help` and `version`, which one should get priority, if they entered both? – codeCompiler77 Mar 19 '16 at 05:48
  • Yes for the behaviour of `--default` followed by `--type` (type overridden or overwritten; file unchanged from default). If both `--help` and `--version` are included, deal with the one you get to first. In my code (which mainly uses single-character options), I use: `-h Print this help message and exit` and `-V Print version and exit` in the help message. When `-h` is encountered, it prints and exits; when `-V` is encountered, it prints and exits. The other option is never spotted by the argument parsing code. – Jonathan Leffler Mar 19 '16 at 05:48
  • Learning a lot, thank you! Just one last question and i'll go try to implement what you suggested :) For `version` and `help`, where would i add their check so that i can do `bash a2.sh --type type-name --default --help` as you suggested? I ask because `[ $# -eq 4 ] || usage` would cut it off and it would never make it to evaluate the `key:value` pairing. – codeCompiler77 Mar 19 '16 at 05:52
  • Never mind :) I add the check just before that! If `error` and `usage` get piped to `>&2` then `help` and `version` would go to `>&1` (stdout) or would it still go to (stderr)? – codeCompiler77 Mar 19 '16 at 05:56
  • Note that the command lines were carefully crafted to include 4 arguments precisely to get past the argument count check. See also my updated answer with the script. If you have things like `--help` to deal with, you don't check the argument count up front. You might check it after the option processing loop, but not before that. (Because you have no trailing arguments, the loop exits with an error when you try to provide one.) See [Using `getopts` in Bash shell script](https://stackoverflow.com/q/402377) and [Short/long options with arguments](https://stackoverflow.com/questions/10818443/). – Jonathan Leffler Mar 19 '16 at 06:09
  • Help and version information are requested output; it should go to stdout. Error messages go to `stderr`. Note that `help` and `version` succeed; the exit status is 0. Note that the `getopts` built-in and `getopt` program deal with `--` as an argument that marks the end of options, even if subsequent arguments start with a `-` character. The code above doesn't handle that. – Jonathan Leffler Mar 19 '16 at 06:09
  • Thanks for updating, i was going about it wrong. I'll edit my post to show how i was going about it. But yours method makes more sense now that i look at it. – codeCompiler77 Mar 19 '16 at 06:11
  • Can you please see my edit `UPDATE - TESTING`, is my usage wrong? – codeCompiler77 Mar 19 '16 at 06:17
  • If you were testing the `d2.sh` above your comment in the question, the case statement in it doesn't test for `-V` so you're getting the correct error. If you're testing my code, you just found a bug; the test for `[ $# -gt 1 ]` should be `[ $# -gt 0 ]` now — the conditions have changed. I've edited my answer to fix that. Spot who didn't test their code before posting it this time! – Jonathan Leffler Mar 19 '16 at 06:20
  • I am testing your code, i just named it `d2.sh`, sorry for the confusion. – codeCompiler77 Mar 19 '16 at 06:21
  • As coded in my script, `d2 -type t --default` and `d2 --default -type t` behave the same. The code looks to see if `$TYPE` is set before setting it to the default; similarly with `$FILE`. That's a minor design decision; there are other ways to cope. One would be to set `FILE=default-filename` and `TYPE=default-typename` before the loop, but then you have to worry about how to check for setting the file name twice, etc. Or the code could blindly overwrite on getting `--default`. Or … None of these are 'wrong'; they're just different (slightly different). – Jonathan Leffler Mar 19 '16 at 06:27
  • I noticed =D `$ bash d2.sh -t 'kjljkl' --default` will assign the remaining values whereas `$ bash d2.sh --default -f data.txt` will complain that it can't overwrite the `f` value. Which is fine with me. I wouldn't want it to anyways. Have a great night! Thank you for taking the time to explain this to me =D – codeCompiler77 Mar 19 '16 at 06:32
  • Oh, yeah — they're not identical, as you identified. As I said, though, you can design the behaviour to match your requirements (as long as you know what you want). Few behaviours are emphatically wrong. If forced to pick ones that are wrong, I'd say "continuing after being requested for help or version information", and "doing anything destructive before validating that the command line is correct". That can be a problem if you do serious actions while processing the options; it is often best to defer such processing until after the option parsing is over. But it always depends on your needs. – Jonathan Leffler Mar 19 '16 at 06:41