Parsing mixed arguments in a script bash - bash

I need to implement a script called with mixed (optional and non-optional) arguments for example -
./scriptfile -m "(argument of -m)" file1 -p file2 -u "(argument of -u)"
in a random order. I've read a lot about the getopts builtin command, but I think it doesn't solve my problem. I can't change the order of arguments, so I don't understand how I can read the arguments one by one.
Someone have any ideas?

You should really give a try to getopts, it is designed for that purpose :
Ex :
#!/bin/bash
while getopts ":a:x:" opt; do
case $opt in
a)
echo "-a was triggered with $OPTARG" >&2
;;
x)
echo "-x was triggered with $OPTARG" >&2
;;
\?)
echo "Invalid option: -$OPTARG" >&2
;;
esac
done
Running the script with different switches ordering :
$ bash /tmp/l.sh -a foo -x bar
-a was triggered with foo
-x was triggered with bar
$ bash /tmp/l.sh -x bar -a foo
-x was triggered with bar
-a was triggered with foo
As you can see, there's no problem to change the order of the switches
See http://wiki.bash-hackers.org/howto/getopts_tutorial

Consider using Python and its excellent built-in library argparse. It will support almost any reasonable and conventional command line options, and with less hassle than bash (which is, strangely, a fairly poor language when it comes to command line argument processing).

Related

Shell script not running on Mac OS

I have what amounts to a very simple bash script that executes a deployment. Here is my code:
#!/usr/bin/env bash
function print_help
{
echo '
Deploy the application.
Usage:
-r reinstall
-h show help
'
}
reinstall=false
while getopts "rh" opt; do
case ${opt} in
r)
echo "clean"
reinstall=true
;;
h)
echo "help"
print_help
exit 0
;;
esac
done
I am calling the script as follows:
. deploy.sh -h
No matter what I do, neither option (i.e. -r, -h) results in the respective echo and in the case of -h the print_help function isn't called.
What am doing wrong?
getopts uses a global variable OPTIND to keep track about which argument it processes currently. Each option it parses, it increments/changes OPTIND to keep track which argument will be next.
If you call getopt without changing OPTIND it will start from where it last ended. If it already parsed first argument, it would want to continue parsing from the second argument, etc. Because there is no second argument the second (or later) time you source your script, there is only -h, getopt will just fail, because it thinks it already parsed -h.
If you want to re-parse arguments in current shell, you need to just reset OPTIND=1. Or start a fresh new shell, which will reset OPTIND to 1 by itself.
If there's a space between the dot and your script name then you're not lauching the script but just sourcing it to your current shell session !
if you want to run your scipt you should do :
# chmod +x ./deploy.sh
# ./deploy.sh -h
if you source it the functtions and variables inside your scipt will be available in your current shell session this could allow you to do things like that :
# . ./deploy.sh
# print_help

Using getopts in Bash

I want to use getopts in a Bash script as follows:
while getopts ":hXX:h" opt; do
case ${opt} in
hXX ) Usage
;;
h ) echo "You pressed Hey"
;;
\? ) echo "Usage: cmd [-h] [-p]"
;;
esac
done
The idea behind is that I want to have two flags -h or --help for allowing user to be able to use HELP in order to be guided how to use the script and another second flag which starts with h but its like -hxx where x is whatever.
How can I distinguish these two since even when I press --hxx flag it automatically executes help flag. I think the order of presenting them in getopt has nothing to do with this.
The 'external' getopt program (NOT the bash built in getopts) has support for '--longoptions'. It can be used as a pre-procssor to the command line options, making it possible to consume long options with the bash built-in getopt (or to other programs that do not support long options).
See: Using getopts to process long and short command line options for more details.
#! /bin/bash
TEMP=$(getopt -l help -- h "$#")
eval set -- "$TEMP"
while getopts h-: opt ; do
case "$opt" in
h) echo "single" ;;
-) case "$OPTARG" in
-help) echo "Double" ;;
*) echo "Multi: $OPTARG" ;;
esac ;;
*) echo "ERROR: $opt" ;;
esac
done

Ambigous args when sourcing another bash file in a bash file

I create a bash file test.sh. The content of this bash is like below:
#!/bin/bash
#source another file
export ICS_START=/rdrive/ics/itools/unx/bin/
source $ICS_START/icssetup.sh
XMAIN=false
MAINLINE=false
STARTDIR=${PWD}
# Get args.
usage() {
echo "Usage: $0 [-t <timestamp>] [-m] [-x]"
exit 1
}
parse_args(){
while getopts "ht:mx" OPT; do
case $OPT in
t) DATE=${OPTARG};;
m) MAINLINE=true;;
x) XMAIN=true;;
h) usage;;
?) usage;;
esac
done
}
echo "$#"
parse_args "$#"
#other commands
myrun -d xxx -p xxx --time xxxx
I run this bash file with ./test.sh -t xxx -m -x
During this process,the second source command is affected by the args -t xxx -m -x, it always throw errors as :
Ambigous switch. Please use more characters. I think icssetup.sh also define these args so we have conflicts with each other. How could I avoid this without changing arg characters?
I checked that the first two lines(source command) and the parse_args can both work well separately.
Any help would be appreciated.
This is something that happens with bash but not other shells. The arguments of your script are passed to any sourced script.
A simple example showing this:
test.sh
#!/bin/bash
source source.sh
echo Original Script: $# : $#
source.sh
#!/bin/bash
echo Sourced Script $# : $#
When you run test.sh, you see that even if no arguments are passed to the sourced script, it actually receives the original script arguments:
# ./test.sh a b
Sourced Script 2 : a b
Original script: 2 : a b
If you're attempting to pass no arguments to the sourced script, you might try to force it like:
source $ICS_START/icssetup.sh ""

Can a shell script flag have optional arguments if parsing with getopts?

I have a script that I want to run in three ways:
Without a flag -- ./script.sh
With a flag but no parameter -- ./script.sh -u
With a flag that takes a parameter -- ./script.sh -u username
Is there a way to do this?
After reading some guides (examples here and here) it doesn't seem like this is a possibility, especially if I want to use getopts.
Can I do this with getopts or will I need to parse my options another way? My goal is to continue using getopts if I can.
The non-getopts example in BashFAQ #35 can cover the use case:
user_set=0 # 1 if any -u is given
user= # set to specific string for -u, if provided
while :; do
case $1 in
-u=*) user_set=1; user=${1#*=} ;;
-u) user_set=1
if [ -n "$2" ]; then
user=$2
shift
fi ;;
--) shift; break ;;
*) break ;;
esac
shift
done

Getopts in sourced Bash function works interactively, but not in test script?

I have a Bash function library and one function is proving problematic for testing. prunner is a function that is meant to provide some of the functionality of GNU Parallel, and avoid the scoping issues of trying to use other Bash functions in Perl. It supports setting a command to run against the list of arguments with -c, and setting the number of background jobs to run concurrently with -t.
In testing it, I have ended up with the following scenario:
prunner -c "gzip -fk" *.out - works as expected in test.bash and interactively.
find . -maxdepth 1 -name "*.out" | prunner -c echo -t 6 - does not work, seemingly ignoring -c echo.
Testing was performed on Ubuntu 16.04 with Bash 4.3 and on Mac OS X with Bash 4.4.
What appears to be happening with the latter in test.bash is that getopts is refusing to process -c, and thus prunner will try to directly execute the argument without the prefix command it was given. The strange part is that I am able to observe it accepting the -t option, so getopts is at least partially working. Bash debugging with set -x has not been able to shed any light on why this is happening for me.
Here is the function in question, lightly modified to use echo instead of log and quit so that it can be used separately from the rest of my library:
prunner () {
local PQUEUE=()
while getopts ":c:t:" OPT ; do
case ${OPT} in
c) local PCMD="${OPTARG}" ;;
t) local THREADS="${OPTARG}" ;;
:) echo "ERROR: Option '-${OPTARG}' requires an argument." ;;
*) echo "ERROR: Option '-${OPTARG}' is not defined." ;;
esac
done
shift $(($OPTIND-1))
for ARG in "$#" ; do
PQUEUE+=("$ARG")
done
if [ ! -t 0 ] ; then
while read -r LINE ; do
PQUEUE+=("$LINE")
done
fi
local QCOUNT="${#PQUEUE[#]}"
local INDEX=0
echo "Starting parallel execution of $QCOUNT jobs with ${THREADS:-8} threads using command prefix '$PCMD'."
until [ ${#PQUEUE[#]} == 0 ] ; do
if [ "$(jobs -rp | wc -l)" -lt "${THREADS:-8}" ] ; then
echo "Starting command in parallel ($(($INDEX+1))/$QCOUNT): ${PCMD} ${PQUEUE[$INDEX]}"
eval "${PCMD} ${PQUEUE[$INDEX]}" || true &
unset PQUEUE[$INDEX]
((INDEX++)) || true
fi
done
wait
echo "Parallel execution finished for $QCOUNT jobs."
}
Can anyone please help me to determine why -c options are not working correctly for prunner when lines are piped to stdin?
My guess is that you are executing the two commands in the same shell. In that case, in the second invocation, OPTIND will have the value 3 (which is where it got to on the first invocation) and that is where getopts will start scanning.
If you use getopts to parse arguments to a function (as opposed to a script), declare local OPTIND=1 to avoid invocations from interfering with each other.
Perhaps you are already doing this, but make sure to pass the top-level shell parameters to your function. The function will receive the parameters via the call, for example:
xyz () {
echo "First arg: ${1}"
echo "Second arg: ${2}"
}
xyz "This is" "very simple"
In your example, you should always be calling the function with the standard args so that they can be processed in the method via getopts.
prunner "$#"
Note that pruner will not modify the standard args outside of the function.

Resources