Home > Software engineering >  Bash script ignores positional arguments after first time used
Bash script ignores positional arguments after first time used

Time:12-02

I noticed that my script was ignoring my positional arguments in old terminal tabs, but working on recently created ones, so I decided to reduce it to the following:

TAG=test

while getopts 't:' c
do
    case $c in
        t)
            TAG=$OPTARG
            ;;
    esac
done

echo $TAG

And running the script I have:

~ source my_script
test
~ source my_script -t "test2"
test2
~ source my_script -t "test2"
test

I thought it could be that c was an special used variable elsewhere but after changing it to other names I had the exact same problem. I also tried adding a .sh extension to the file to see it that was a problem, but nothing worked.

Am I doing something wrong ? And why does it work the first time, but not the subsequent attempts ?

I am on MacOS and I use zsh.

Thank you very much.

CodePudding user response:

There are a few issues with your script that may be causing it to behave unexpectedly.

First, you are using the getopts command to parse command-line arguments, but you are not specifying which options are valid. The getopts command expects the list of valid options to be provided in the first argument, before the variable to store the parsed option in. For example, you could use the following syntax to parse the -t option:

while getopts 't:' c
do
    case $c in
        t)
            TAG=$OPTARG
            ;;
    esac
done

Second, the source command is used to execute a script in the current shell, which means that any changes made to variables in the script will persist after the script has finished running. In your script, you are using the source command to run the script, but then you are overwriting the value of the TAG variable with a new value. This means that the value of the TAG variable will always be reset to its initial value whenever the script is run using the source command.

To avoid this issue, you could use the . (dot) operator instead of the source command to run the script in the current shell. The . operator has the same effect as source, but it does not reset the value of variables after the script has finished running. For example, you could use the following syntax to run your script using the . operator:

. my_script

Alternatively, you could use the bash command to run the script in a new shell, which would prevent the value of the TAG variable from being reset. For example, you could use the following syntax to run your script using the bash command:

bash my_script

I hope this helps!

CodePudding user response:

The problem is that you're using source to run the script (the . command does the same thing). This makes it run in your current (interactive) shell (rather than a subprocess, like scripts normally do). This means it uses the same variables as the current shell, which is necessary if you want it to change those variables, but it can also have weird effects if you're not careful.

In this case, the problem is that getopts uses the variable OPTIND to keep track of where it is in the argument list (so it doesn't process the same argument twice). The first time you run the script with -t test2, getopts processes those arguments, and leaves OPTIND set to 3 (meaning that it's already done the first two arguments, "-t" and "test2". The second time you run it with options, it sees that OPTIND is set to 3, so it thinks it's already processed both arguments and just exits the loop.

One option is to add unset OPTIND before the while getopts loop, to reset the count and make it start from the beginning each time.

But unless there's some reason for this script to run in the current shell, it'd be better to make it a standard shell script and have it run as a subprocess. To do this:

  1. Add a "shebang" line as the first line of the script. To make the script run in bash, that'd be either #!/bin/bash or #!/usr/bin/env bash. For zsh, use #!/bin/zsh or #!/usr/bin/env zsh. Since the script runs in a separate shell process, the you can run bash scripts from zsh or zsh scripts from bash, or whatever.

  2. Add execute permission to the script file with chmod -x my_script (or whatever the file's actual name is).

  3. Run the script with ./my_script (note the lack of a space between . and /), or by giving the full path to the script, or by putting the script in some directory in your PATH (the directories that're automatically searched for commands) and just running my_script. Do NOT run it with the bash, sh, zsh etc commands; these override the shebang and therefore can cause confusion.

Note: adding ".sh" to the filename is not recommended; it does nothing useful, and makes the script less convenient to run since you have to type in the extension every time you run it.

Also, a couple of recommendations: there are a bunch of all-caps variable names with special meanings (like PATH and OPTIND), so unless you want one of those special meanings, it's best to use lower- or mixed-case variable names (e.g. tag instead of TAG). Also, double-quoting variable references (e.g. echo "$tag" instead of echo $tag) avoids a lot of weird parsing headaches. Run your scripts through shellcheck.net; it's good at spotting common mistakes like this.

  • Related