3

I have a script where I've implemented switches using getopts. However, I'm having trouble referencing the next argument.

My script is for backporting a backup of our website on a local development environment. I've added a -p switch to run some post-deploy steps. Here's my syntax:

backport -p /path/to/website_backup.sql.gz

So, before the switch, I was testing that a file was specified, and that it's a proper file. Since a filename path was the only argument, I could assume that it was necessary, and also that it would be the first argument ($1).

if [[ $# -eq 0 ]] ; then
  echo 'Specifcy the sql file to backport.';
  exit 0;
fi

if [[ ! -f "$1" ]]; then
  echo "$1 is not a valid file.";
  exit 0;
fi

I found this answer which demonstrated an example of how to use getopts to parse switch arguments:

while getopts "p" opt; do
  case $opt in
    p) p_post_deploy=true ;; # Handle -a
  esac
done

Of course, using $1 as the argument didn't work after implementing getopts. Without the switch it was fine. But, when I added the switch, it was, of course, the first argument, which my script was testing for being a file.

$ backport -p /path/to/website_backup.sql.gz
-p is not a valid file.

So, with the introduction of switches, I can't rely on any argument appearing at particular position in the command. Hard-coding $2 won't work, because the filename argument won't be the second argument if there is no switch. I want a solution that will allow me to accept arguments after switches, and allow me to introduce new switches in the future while only updating the code to handle the new switches themselves (and not have to re-shuffle later arguments that may be moved after the introduction of a new switch).

I looked at the answers to the unix.stackexchange question that instructed me how to use getopts to parse switches. One of the answers mentioned $* as a variable representing the remaining arguments.

if [[ "$*" -eq 0 ]] ; then
  echo 'Specifcy the sql file to backport.';
  exit 0;
fi

However, when I try to use it, I'm not expressing syntax correctly, and I get a parse error.

~/scripts/backport: line 14: [[: /d/Downloads/database.sql.gz: syntax error: operand expected (error token is "/d/Downloads/database.sql.gz")

How do I test the filename argument after getops?


Here is the script in its current version:

$ cat backport
#!/bin/bash

set -e

while getopts "p" opt; do
  case $opt in
    p) p_post_deploy=true ;;
  esac
done

shift $(($OPTIND - 1))
# testing what this variable looks like
printf "Remaining arguments are: %s\n" "$*"

if [[ "$*" -eq 0 ]] ; then
  echo 'Specifcy the sql file to backport.';
  exit 0;
fi

if [[ ! -f "$*" ]]; then
  echo "$* is not a valid file.";
  exit 0;
fi

drush @local.dev sql-drop -y ;
zcat $1 | drush @local.dev sqlc ;
drush @local.dev cr;

if [ ! -z "$p_post_deploy" ] ; then
  echo "Running post-deploy..."
  SCRIPT_PATH=$(dirname "$BASH_SOURCE")
  source "$SCRIPT_PATH/post-deploy"
  post_deploy
fi
user394
  • 14,404
  • 21
  • 67
  • 93
  • can you add a minimalist version of you script using getopts – EchoMike444 Oct 31 '19 at 01:33
  • @EchoMike444 I've edited to include the current form of the script – user394 Oct 31 '19 at 13:07
  • Could you expand on "didn't work after implementing getopts"? What did your script look like after you'd added getopts, and what error did you encounter, when you were still trying to use $1 (ie. before you added those "$*", which are causing other problems)? – JigglyNaga Oct 31 '19 at 13:26
  • @JigglyNaga Since $1 is a positional argument, the switch -p is the value of $1 when it is used. I don't want to test that $1 is a file, because it could be the switch. See my edits to the question – user394 Oct 31 '19 at 13:53
  • 2
    @user394 After shift "$(( OPTIND - 1 ))", the value of $1 will be the first argument after any option, not -p (this is why you do that shift, after all) – Kusalananda Oct 31 '19 at 13:54
  • @Kusalananda Thanks, that seems to be the approach I need. Could you post that as an answer, so I can accept it? – user394 Oct 31 '19 at 13:59
  • @user394 Hmm... that is not an answer to your question though. The only clear error that you have in your code s the one that AdminBee has already mentioned. Using $* is not an error, but it's clearer to use $1 if you want to refer to the first remaining argument explicitly. – Kusalananda Oct 31 '19 at 14:02
  • @Kusalananda Okay; follow-up: If I understand correctly, shift "$(( OPTIND - 1 )) is hard-coded one argument out of the list-- in my case, the switch -p. Is there a way to specify to pull all switches out, after parsing them, so that I can introduce new switches to the script in the future, without having to remember to update the shift line? – user394 Oct 31 '19 at 14:10
  • 2
    @user394 No, you misunderstand the statement and what OPTIND is. The OPTIND variable holds the position of the first command line argument that is not an option. Shifting by this number, minus 1, always leaves the non-options. No need to update that statement ever. – Kusalananda Oct 31 '19 at 14:17

2 Answers2

2

I can only blame myself for poor communication, but Kusalananda has given the answer I was looking for in a comment.

After using getopts to parse switches

while getopts "p" opt; do
  case $opt in
    p) p_post_deploy=true ;;
  esac
done

this line

shift "$((OPTIND - 1))"

will remove all switches from the list of arguments, so that you can use positional arguments again, just as you would without switches.

The shell does not reset OPTIND automatically. You have to reset it manually.

Kusalananda
  • 333,661
user394
  • 14,404
  • 21
  • 67
  • 93
0

Your application of the test construct

if [[ "$*" -eq 0 ]]

is incorrect. -eq is used for integer comparisons, and given that $* refers to the argument list of all remaining arguments, not their number, your test construct is bound to produce a syntax error unless the list of remaining arguments at that point happens to contain only one element, which must also happen to represent an integer number (which is not what you want to achieve).

Now, for the actual task, I think it is difficult to mix "non-option parameters" (which are not preceded by an "announcing" -<some letter>) with option parameters as long as you allow an arbitrary order (i.e. as long as you do not specify that the filename must come last). If you want to use getopts, you may simply want to make the filename an option parameter too, by requiring it to be specified as -f <filename>. Then you can use

while getopts "pf:" opt; do
  case $opt in
    p)
       p_post_deploy=true
    ;;
    f)
       backportfile=$OPTARG
    ;;
  esac
done

and have getopts automatically complain if no filename is specified.

./test.sh: option requires an argument -- f

You can then still test later if the file exists, as

if [[ ! -f "$backportfile" ]]
then
  echo "$backportfile is not a valid file"
  exit 1
fi
AdminBee
  • 22,803
  • Notice how the text in the question uses [[ $# -eq 0 ]] first (which is correct), but how the more complete code uses [[ "$*" -eq 0 ]] for the same test. – Kusalananda Oct 31 '19 at 13:40
  • @Kusalananda Yes, I noticed it. You mean it could have been a copy-and-paste error? – AdminBee Oct 31 '19 at 13:42
  • Or a re-typing error. In any case, $# -eq 0 is closer to what I think their intent was. You could also comment on the use of unquote variable expansions. – Kusalananda Oct 31 '19 at 13:53
  • I believe I've miscommunicated: -p does not require an argument; it's strictly an optional switch to the command. The command itself always requires an argument. – user394 Oct 31 '19 at 14:18
  • Yes, I didn't get that part of your post correctly. Maybe my revised suggestion helps ... – AdminBee Oct 31 '19 at 14:34
  • "unless the list of remaining arguments at that point happens to contain only one element, which must also happen to represent an integer number" -- it doesn't have to be an integer, a string that's a valid name of a variable works too. It'll be expanded, recursively, and if it's undefined, it's equal to zero. That is to say set -- foo; [[ "$*" -eq 0 ]] && echo yes prints yes if foo hasn't been previously set – ilkkachu Oct 31 '19 at 16:46