8

I have a script that users will prefix, rather than append, arguments to, i.e. they might call command C, command B C, command A B C, and so on.

I'd like to be able to simply shift over these arguments from the right, the same way you might shift them from the left with shift.

I'm imaginging a shift-right command that behaves like so:

echo "$@"    # A B C
shift-right
echo "$@"    # A B
shift-right
echo "$@"    # A
shift-right
echo "$@"    # 
echo "$#"    # 0

Is there a clean way to accomplish this? I know I can work around it, but a shift-like solution would be much nicer and simpler.


In response to the XY-problem comment, my specific use case is a command that takes either a port or a host and port, e.g. command 123 or command remotehost 123. I don't want users to have to specify these in reverse order (port then host).

It would be fairly clean to say something like (untested, obviously):

port=${@: -1}
shift-right
host=${1:-localhost}

Really though, I'm curious about the question in general, even if there's a better way to solve this specific example.

Here's one reasonably clean way to handle the two-argument case without shift-right, just for reference:

port=${@: -1}
host=${2:+$1}
host=${host:-localhost}

But hopefully you can appreciate how that becomes more cludgy as the number of arguments increases.

5
  • This is almost certainly a case of the XY Problem. Would you mind editing your question to explain what you're ultimately trying to accomplish? (Or if this a case of pure intellectual interest, edit your question to specify that.)
    – Wildcard
    Commented Mar 31, 2016 at 23:39
  • @Wildcard added a specific use-case, but I'm asking primarily out of curiosity.
    – dimo414
    Commented Apr 1, 2016 at 0:10
  • as @Wildcard says, the only safe way to have optional arguments is to use getopts or similar (e.g. /usr/bin/getopt from the util-linux package if you want to support --long options as well as short).
    – cas
    Commented Apr 1, 2016 at 22:37
  • But beware of using getopt in scripts. It's not even part of bash, so it introduces an external dependency. Super un-portable. @cas
    – Wildcard
    Commented Apr 1, 2016 at 22:46
  • Which is mostly why I mentioned where it came from. The other reason is that other, non-util-linux, versions of getopt are known to be buggy and unsafe to use. getopt from util-linux is available for and/or can be compiled for other unixes. if you want/need --long options in shell scripts, it's really the only choice available. BTW, even SC's getopts_long shell function uses perl and the Getopt::Long perl module, which are also external dependencies.
    – cas
    Commented Apr 1, 2016 at 22:54

3 Answers 3

14

If the list of positional parameters is:

$ set -- 0wer 1wdfg 2erty 333 4ffff 5s5s5

Then this will print the arguments without the last:

$ echo "${@:1:$#-1}"
0wer 1wdfg 2erty 333 4ffff

Of course, the parameters could be set to that as well:

$ set -- "${@:1:$#-1}"
$ echo $@
0wer 1wdfg 2erty 333 4ffff

That works in bash version 2.0 or above.

For other simpler shells, you need a (somewhat tricky) loop to remove the last parameter:

unset b; for a; do set -- "$@" ${b+"$b"}; shift; b="$a"; done
2
  • That last loop is not correct; it results in a zero-length argument being set as the first parameter. E.g. run with the parameters "1" "2" "3" it results in the parameters being set as "" "1" "2".
    – Wildcard
    Commented Apr 1, 2016 at 0:58
  • @Wildcard And indeed you are right. Thanks for reminding me of this. The command printf '<%s>' "$@" will print <><0wer><1wdfg><2erty><333><4ffff>. Solution? ... Maybe that is why I unset the variable b. Read the edited answer.
    – user79743
    Commented Apr 1, 2016 at 1:15
1

You can simply check the number of arguments and handle them in reverse order, e.g. for the simple two argument case where the first argument is optional:

usage() {
  cat << EOF
Usage: $0 [host] port
EOF
}

[ "$#" -eq 0 ] && { usage; exit 1;}

if [ "$#" -gt 1 ]; then
  myhost="$1"
  shift
fi

myport="$1"

However you mention the idea of three arguments as well. It's not really a trivial matter to have three optional arguments that are determined by position—what if you want to specify the first and third but not the second? Such scenarios are nearly impossible to handle with positional parameters alone but very simple with correct use of getopts.

usage() {
  cat << EOF
usage: $0 [OPTIONS]

    -p    port number (required)
    -h    hostname (optional)
EOF
}

while getopts :p:h: opt; do
  case "$opt" in
    p)
      myport="$OPTARG"
      ;;
    h)
      myhost="$OPTARG"
      ;;
    :)
      printf %s\\n "Argument required for option -$OPTARG" >&2
      usage >&2
      exit 1
      ;;
    \?)
      printf %s\\n "Unknown option -$OPTARG" >&2
      usage >&2
      exit 1
      ;;
  esac
done
shift "$((OPTIND-1))"

if [ -z "$myport" ]; then
  printf %s\\n "You must supply a port" >&2
  usage >&2
  exit 1
fi
2
  • The "first and third but not second" argument applies equally to commands that shift left; as soon as you start needing arbitrary subsets of the arguments you should be using flags. That doesn't mean that programs with optional positional parameters are fundamentally flawed.
    – dimo414
    Commented Apr 1, 2016 at 0:45
  • @dimo414 I'm well aware of that. As you can see I've now included the code example using flags. I just decided to post my answer without waiting until I finished the second example, since it's still useful without. Nothing wrong at all with optional positional parameters; I would happily use either of the code examples above in production scenarios.
    – Wildcard
    Commented Apr 1, 2016 at 0:52
0

Coming back to my own question, today I would probably simply implement this with a separate array and check the number of passed arguments, something like:

# Just a demo of a command that "shifts right"
# Usage: url_builder [[[scheme] hostname] port] 
url_builder() {
  local parts=(http localhost 80) # defaults
  if (( $# == 3 )); then
    parts[0]=$1; shift
  fi
  if (( $# == 2 )); then
    parts[1]=$1; shift
  fi
  if (( $# == 1 )); then
    parts[2]=$1; shift
  fi
  printf '%s://%s:%s\n' "${parts[@]}"
}

Usage:

$ url_builder
http://localhost:80

$ url_builder 100
http://localhost:100

$ url_builder example.com 100
http://example.com:100

$ url_builder ftp example.com 100
ftp://example.com:100

Whether that's better than the set approach suggested by user79743 probably depends on the use-case, but for anything more than a couple of arguments I'd start to get skeptical of the pattern in the first place. Users would likely be somewhat surprised by a command with numerous positional arguments that are optional further to the left like this.

You must log in to answer this question.

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