How do I make an option accept an arbitrary number of arguments - shell

I am trying to write a shell script with options that take different numbers of values in each run, e.g.
Run 1:
./myscript --input <file1> <file2> --output <folder1>
Run 2:
./myscript --input <file1> <file2> <file3> --output <folder1> <folder2>
I tried using getopts as follows:
while getopts ":i:o:" opt; do
case $opt in
i)
echo "treatment files are: $OPTARG" >&2
infiles="${OPTARG}"
;;
o)
echo "control files are: $OPTARG" >&2
outdir="${OPTARG}"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
esac
done
However, the only way I found to input variable amounts of values for the "-i" option is by quotation ("file1 file2"), which prevents me/the user to use the autocomplete function of the command line to help generating the file path. Also this option does not seem allow me to use "--input" syntax to pass the options.
Anybody can help?
Thanks a lot.

You have two questions here. One of them is "how do I implement long options in my shell script?", for which there is good reading here.
The other question is, "how do I make an option accept an arbitrary number of arguments?" The answer there is, mostly, "you don't". Often the cleanest solution is to have your script accept the option multiple times, as in:
./myscript -i <file1> -i <file2> -i <file3>
This is relatively easy to handle by appending to a Bash array, as in:
infiles=()
while getopts ":i:o:" opt; do
case $opt in
i)
infiles+=("${OPTARG}")
;;
esac
done
And later on you can iterate over those values:
for inputfile in "${infiles[#]}"; do
...
done
Read about bash arrays for more information.
If you don't like that solution, you could implement your own option parsing in a manner similar to some of the answers to the question to which I linked in the first paragraph. That would allow you to get the command line semantics you want, although I would argue that behavior is at odds with the way a typical program behaves.

Related

How to getopts after $1 in bash [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
I want to run the script as ./script speed -a some_value -b some_value also ./script accuracy -a some_value -b some_value
What I tried is
while [ -n "$1" ]; do
case "$1" in
speed)
for i in "${#:2}"
do while getopts "a:b:" opt; do
case "${opt}" in
a) list=$OPTARG
echo $list
;;
b) list2=$OPTARG
echo $list2
;;
esac
done
done
echo "speed option passed"
break ;;
accuracy) echo "similar to above function"
break ;;
*) echo "Option $1 not recognized" ;; # In case you typed a different option other than a,b,c
esac
shift
done
getting output as when ran ./script speed -a some_value
this is something
speed option passed
I don't know if this is possible or not or is there any way to do something like this?
I don't think you want the outer loop (while [ -n "$1" ]; do), unless you want to be able to process multiple subcommands in a single run. That is, do you want this:
./script speed -a some_value -b some_value accuracy -a some_value -b some_value
To be roughly equivalent to this:
./script speed -a some_value -b some_value
./script accuracy -a some_value -b some_value
If not, remove that loop because you'll only be processing one subcommand per run. If you do want to process more than one subcommand per run, then you need to take some extra steps to remove or skip over the arguments relating to one subcommand before running the next one.
You do want to remove the for i in "${#:2}" loop -- that just doesn't mix with the way getopts works. What you do need to do is skip over the subcommand name before processing the options. You could either use shift to remove the subcommand name, something like this:
case "$1" in
speed)
shift # Remove the first argument ("speed")
while getopts "a:b:" opt; do
...
If you're going to allow multiple subcommands, add shift $((OPTIND-1)) sfter the getopts loop, to get it ready for the next subcommand.
Or you could modify OPTIND to tell getopts that it's already processed the first argument and it can go to work on the second:
case "$1" in
speed)
OPTIND=2 # Tell getopts to start processing at arg #2
while getopts "a:b:" opt; do
...
If you're going to handle multiple subcommands with this method... well, it's a bit more complicated and I think I'll duck the question.
Yet another option is to put the code for each subcommand in a function, and call it with all but the first argument:
speed_subcommand() {
local OPTIND
while getopts "a:b:" opt; do
...
}
case "$1" in
speed)
speed_subcommand "${#:2}" ;;
accuracy)
accuracy_subcommand "${#:2}" ;;
...
This method doesn't really mix with handling multiple subcommands per run.

bash - getopts in switch case

I'm trying to use getopts inside a switch case loop.
if i use only getopts or only the switch case it's work, however when i combine those two the getopts dos not trigger.
i have search a lot but i cat fins any mention for how to combine them, and problem i missing something stupid so for give me ...
here is the code essence.
#!/bin/bash
case $1 in
ver)
echo "vesion"
exit 0
;;
op)
while getopts ":a" opt; do
case $opt in
a)
echo "-a was triggered!" >&2
;;
\?)
echo "Invalid option: -$OPTARG" >&2
;;
esac
done
;;
esac
when i do that
# bash -x test.sh op -a
i get
+ case $1 in
+ getopts :a opt
(and without debug i get nothing)
what is that that i missing to combine these two
Thanks :)
You should add a shift instruction at the beginning of your op) choice, before the call to getopts, to eat the op argument itself. Else, the first argument that getopts will analyze is op and it will silently stop (end of options).

Using getopts with a switchless path

I am trying to write a short script, utilizing getopts. I want it to take optional switches, or just run as the default. I have a -d switch to enable debugging, and I'd like every other argument to be a path. The ideal command line looks as such, with paths being optional, and theoretically limitless:
$0 [-d] [/path1[ /path2[ ...]]]
I am currently using getopts as such below:
while getopts ":d" opt; do
case $opt in
d)
DEBUG=true
;;
h)
echo USAGE: $0 \[-d\] \[\/mount\/point\/1 ...\]
exit 0
;;
\?)
echo Incorrect syntax
;;
esac
done
What can I put in the while getopts section, and in the case set, to allow paths to be entered, as many as needed?
You don't need anything in the loop or getopts call for that. getopts stops at the first non-option.
After your loop all your paths will still be in positional arguments available for use.
Also you don't have h in your getopts string so it isn't valid.

Bug in parsing args with getopts in bash

I was trying to modify the bd script to use getopts. I am a newbie at bash scripting
my script is
while getopts ":hvis:d:" opt
do
...
done
...
echo $somedirpath
cd "$somedirpath"
this runs fine when doing
$ ./bd -v -i -s search
or
$ ./bd -is search -d dir
But when running it like this
$ . ./bd -s search
getopts doesn't read the arguments at all. And all the variables I set in the while loop according to the arguments are all not set, so the script no longer works. Please help!
Setting OPTIND=1 before invoking getopts works fine.
The problem is that getopts relies on OPTIND to loop through the arguments provided, and after sourcing the script, it will be set to some value greater than 1 by getopts according to how many arguments you pass. This value gets carried over even after the script ends(because its being sourced). So the next time its sourced, getopts will pick up from that OPTIND, rather than starting from 1!
This might cause strange behaviour with other scripts, and I don't know how safe this is. But it works!
For a better workaround, I think what #tripleee suggests looks safe and robust.
When you source a script, the arguments parsed by getopts are those of the current shell, not the parameters on the source command line.
The common workaround is to have your script merely print the path, and invoke it like cd "$(bd)" instead (perhaps indirectly through a function or alias).
Setting OPTIND=1 may not work reliably on zsh. Try to use something different than getopts:
while [ "$#" -gt 0 ]
do
case "$1" in
-h|--help)
help
return 0
;;
-o|--option)
option
return 0
;;
-*)
echo "Invalid option '$1'. Use -h|--help to see the valid options" >&2
return 1
;;
*)
echo "Invalid option '$1'. Use -h|--help to see the valid options" >&2
return 1
;;
esac
shift
done

How to write a command line tool using bash

I want to write a command line tool like git which will follow the POSIX standards. It will take the options like --help or -h , --version ..etc. But i am not getting how to do it. Can anybody tell me how to do this using bash scripting. Please help me. This is something very new to me.
Example : if the name of my tool is Check-code then i want to use the tool like ;
Check-code --help
or
Check-code --version
So far as I know, "long options", like --help and --version are not POSIX standard, but GNU standard. For command-line utilities the POSIX standard says:
The arguments that consist of hyphens and single letters or digits, such as 'a', are known as "options" (or, historically, "flags").
To support POSIX short options options it is worth getting to know getopts (there are tutorials on the web), but it does not support GNU long options.
For long options you have to roll your own:
filename=default
while (( $# > 0 ))
do
opt="$1"
shift
case $opt in
--help)
helpfunc
exit 0
;;
--version)
echo "$0 version $version"
exit 0
;;
--file) # Example with an operand
filename="$1"
shift
;;
--*)
echo "Invalid option: '$opt'" >&2
exit 1
;;
*)
# end of long options
break;
;;
esac
done
You can use the 'getopts' builtin, like so:
#!/bin/bash
# Parse arguments
usage() {
echo "Usage: $0 [-h] [-v] [-f FILE]"
echo " -h Help. Display this message and quit.
echo " -v Version. Print version number and quit.
echo " -f Specify configuration file FILE."
exit
}
optspec="hvf:"
while getopts "$optspec" optchar
do
case "${optchar}" in
h)
usage
;;
v)
version
;;
f)
file=${OPTARG}
;;
*)
usage
;;
esac
done
This only works for single character options, not for long options like -help or --help. In practice, I've never found that this is a significant restriction; any script which is complex enough to require long options is probably something that I would write in a different language.
There is probably a better way to do this, but here is what I find useful:
Each argument is represented by a variable in BASH. The first argument is $1. The second is $2, and so on. Match an option string with the first argument, and if it matches run some code accordingly.
Example:
#!/bin/bash
if [ $1 == "--hello" ]
then
echo "Hello"
else
echo "Goodbye"
fi
If you code in C or C++, then use the **argv variable. **argv is a double pointer that holds a list of all arguments passed to the program (with argv[0] being the program name).

Resources