Skip to main content
4 of 7
added 95 characters in body
Stéphane Chazelas
  • 584.8k
  • 96
  • 1.1k
  • 1.7k

First, expecting options after non-option arguments is bad practice.

The standard command line interface is to have options before non-option arguments, and allow an optional -- to mark the end of options so you can accept non-option arguments that happen to start with -:

myscript -c -ofile.out -- -file1-.in -file2-.in

Same as:

myscript -co file.out -- -file1-.in -file2-.in

Where, as an example -o is an option that takes an argument, -c one that doesn't, -- marks the end of options, and -file1-.in, -file2-.in are two non-option arguments, taken as such despite starting with - because they are after the -- end-of-option argument.

You can use the standard getopts shell builtin to process options with that standard convention.

On Linux, you can also use the getopt utility from util-linux that handles standard options, but also GNU-style long-options or options that take optional arguments, and allows options after non-option arguments (unless $POSIXLY_CORRECT is in the environment), but still honours -- as the end-of-option marker. getopt will take care of reordering the arguments correctly with options before non-options.

To handle input to be given either via command line or stdin, you'd do:

while getopts...; do
  # process options
  ...
done
# option processing done.
shift "$((OPTIND - 1))"
# now the positional parameters contain non-option arguments

if (( $# )); then
  args=("$@")
else
  # no non-option arguments, read them one per line from stdin
  readarray -t args
fi

The ((...)) is a kshism (also supported by bash), readarray a bashism. In sh, you'd do:

if [ "$#" -eq 0 ]; then
  # no non-option arguments, read them from stdin
  IFS='
' # split on newline
  set -o noglob
  set -- $(cat) # read and split+glob with glob disabled
fi

Where the result is stored in the positional parameters ($1, $2... "$@") instead of the $args array in the bash example above. Beware that in that case, empty lines are discarded.

Also note that file paths can contain any byte value except 0, and that includes the newline character, so you can't pass an arbitrary list of file paths one per line.

Above, we only ready from stdin if no non-option argument is passed on the command line.

If you want to allow input to be provided both via arguments or stdin, you'd have to always read from stdin:

readarray -t args
args+=("$@")

To combine both into $args.

When the user doesn't want to pass any via stdin, they can always do:

myscript arg1 arg2 < /dev/null

To pass the list interactively when stdin is a terminal, you'd type the file list and end it with Ctrl+d.

Stéphane Chazelas
  • 584.8k
  • 96
  • 1.1k
  • 1.7k