I have a bash script which processes an input file with optional arguments. The script looks like this

while getopts a:b:i: option
case "${option}"
a) arg1=${OPTARG};;
b) arg2=${OPTARG};;
i) file=${OPTARG};;

[ -z "$file" ] && { echo "No input file specified" ; exit; }

carry out some stuff

The script runs fine, but I need to specify the input file like so

sh script.sh -a arg1 -b arg2 -i filename

I would prefer to be able to call the script without the -i option, like so

sh script.sh -a arg1 -b arg2 filename

while still having the error message when no input file is specified. Is there a way to do this?

  • And what should happen if the script is called as script.sh -i file1 -i file2 file3 file4? Commented Oct 29, 2020 at 15:02
  • @Stéphane-Chazelas Hm, I don't care really (it's for my personal use only). It should just complain (and more importantly, do nothing) if I write something like sh script.sh -a arg1 or sh script.sh -a arg1 -b arg2.
    – 220284
    Commented Oct 29, 2020 at 15:07

2 Answers 2

#!/bin/sh -

# Beware variables can be inherited from the environment. So
# it's important to start with a clean slate if you're going to
# dereference variables while not being guaranteed that they'll
# be assigned to:
unset -v file arg1 arg2

# no need to initialise OPTIND here as it's the first and only
# use of getopts in this script and sh should already guarantee it's
# initialised.
while getopts a:b:i: option
  case "${option}" in
    (a) arg1=${OPTARG};;
    (b) arg2=${OPTARG};;
    (i) file=${OPTARG};;
    (*) exit 1;;

shift "$((OPTIND - 1))"
# now "$@" contains the rest of the arguments

if [ -z "${file+set}" ]; then
  if [ "$#" -eq 0 ]; then
    echo >&2 "No input file specified"
    exit 1
    file=$1 # first non-option argument

if [ "$#" -gt 0 ]; then
  echo There are more arguments:
  printf ' - "%s"\n' "$@"

I changed the bash to sh as there's nothing bash-specific in that code.


I would like to expand on Stéphane Chazelas' excellent answer in order to use bashism more on the question style, namely [ -z "$file" ] && { echo "No input file specified" ; exit; } -- instead of using ifs.

unset -v file arg1 arg2

while getopts "a:b:i:" option; do
    case "$option" in
        a ) arg1="$OPTARG";;
        b ) arg2="$OPTARG";;
        i ) file="$OPTARG";;
        * ) exit 1;;

shift "$((OPTIND - 1))"

[ -z "$file" ] && [ "$#" -eq 0 ] && { echo "No input file" >&2; exit 1; }
[ -z "$file" ] && [ "$#" -gt 0 ] && { file="$1"; shift; }

echo "DEBUG: arg 1 is $arg1"
echo "DEBUG: arg 2 is $arg2"
echo "DEBUG: file  is $file"

[ "$#" -gt 0 ] && { echo "More arguments"; printf " - %s\n" "$@"; }
  • That's helpful too. Thanks!
    – 220284
    Commented Nov 12, 2020 at 9:32
  • Not sure what you mean there. && / || are not bashisms. They were already present in the Bourne shell in the 70s. They're also present in csh which make them more portable than if whose syntax is different between Bourne-like shells and csh. Using them in place of if constructs is often considered bad practice though. Note that [ -z "$file" ] would also return true if the script is called with -i '' (likely not a problem in practice) Commented Nov 29, 2021 at 15:28

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .