getopts in bash scripts - bash

I am trying to understand a piece of bash script that uses getopts.
I have
#!/bin/bash
function Argparser () {
while getopts 'o:dfath' arg "$#"; do
case $arg in
'o')
echo "oooooh"
output_dir=${OPTARG}
;;
'd')
echo 'ddddddd'
use_data_calib=true
;;
?)
echo "UNKNWON ARGS: ${OPTARG} "
exit 1
;;
esac
done
}
#----------------------------------------
# user parameters
#----------------------------------------
# data directory(required)
data_root_dir=$1
# output directory
output_dir=${data_root_dir}/videos
declare others=${#:2}
Argparser ${others}
#declare use_data_calib=false #<<-----HERE
echo ${output_dir}
echo ${data_root_dir}
echo ${others}
echo ${use_data_calib}
First I would like to understand what dfath does, and what arguments does this expect by using o:dfath.
How should I call this script and with what options?
Another thing that called my attention was the commented line (HERE) . I commented it and now I can set use_data_calib to true or false. However in the original code I am reading the line was not commented.
Doesn't that line defeat the purpose of the argument d? Because with that line, use_data_calib is always false...

o:dfath is just a string that refers to what options the script takes.
The letters are all of the options it can take, as in, -o, -d, etc.
Letters to the left of the colon indicate options which expect an argument, like -o myargument. Letters to the right of the colon do not expect an argument.
To your second point, I believe you are correct. The script you are working with is likely incomplete or incorrect.

Related

How to control redirection operator passed as one of the arguments to a bash function?

I'm trying to script some long/repetitive configuration & build operations in bash.
Started with a function that displays given command plus args and then executes it with given args.
Function definition follows:
runit () {
cmd=${#}
echo "${cmd}"
${cmd}
}
runit touch /tmp/cltconf1
Above (not involving redirection operator) displays the command and touches the target file as expected.
runit echo "gEnableSecureClient=True" > clt1.conf
Above (involving redirection operator) doesn't display command before execution and the content of clt1.conf file after the execution is:
echo gEnableSecureClient=True
gEnableSecureClient=True
I could understand that the redirection is not being controlled and thus causing the echo ${cmd} to actually write content echo gEnableSecureClient=True to clt1.conf and then actual command execution then writes content gEnableSecureClient=True.
I want to find out if this redirection operator can be controlled for my requirement.
Any shopts or escape sequence handling would help.
Your question is:
How to control redirection operator passed as one of the arguments to a bash function?
Invent your own invention and use it to pass the context (filename to be redirected to) to your function. You could use a global variable or you could use positional arguments with some rarely used argument style, like for example ++, for example:
runit() {
# parse arguments - detect `++` followed by `>` and filename
local cmd
while (($#)); do
case "$1" in
"++") break; ;;
*) cmd+=("$1"); ;;
esac
shift
done
local outf=/dev/stdout
if (($#)) && [[ "$1" == "++" ]]; then
shift
while (($#)); do
case "$1" in
">") outf=$2; shift; ;;
*) echo "ERROR: Invalid arguments: expected >" >&2; return 1; ;;
esac
shift
done
fi
if (($#)); then
echo "ERROR: internal error when parsing arguments" >&2; return 1
fi
echo "Running ${cmd[*]} > $outf"
"${cmd[#]}" > "$outf"
}
runit echo "gEnableSecureClient=True" ++ '>' clt1.conf
or example with global var, way simpler, but more spagetti:
runit() {
if [[ -n "${runit_outf:-}" ]]; then
echo "$* > $runit_outf"
"$#" > "$runit_outf"
runit_outf= # let's clear it after use
else
echo "$*"
"$#"
fi
}
# outputs to stdout
runit echo 123
runit_outf=clt1.conf # spaghetti code
runit echo 123 # outputs to file
This is just a template code that I did not test and have written in StackOverflow. It will not handle file descriptors - for that, you could write your own logic that would either parse the expression - ie. detect & in >&1, or write very unsafe code and call eval.
The presented above way is not recommended in any way, rather I wouldn't ever write code like that and would strongly discourage writing such complicated logic to handle simple cases. Instead, you should differentiate the output of a command from the logging stream of command, typically you would:
runit() {
local cmd
cmd=("$#")
echo "${cmd[*]}" >&2
"${cmd[#]}"
}
or use a dedicated beforehand-opened file descriptor in your code or redirect output to /dev/tty depending on the situation.
Before continuing further, you should definitely research what are and when to use bash arrays, research word splitting, and filename expansion, and check your script with https://shellcheck.net . Your function as it is now will break in surprisingly many ways when passing arguments with spaces and with special characters like * or ?.

Print out a list of all cases of a switch

Curious question. Is it somehow possible to print out all cases of a certain switch-case automatically in bash? In a way, such that it stays as maintainable as possible, meaning that one does not have to add any more code if a new case is added to print out that same case.
For instance, that would be useful if the cases represented commands. A help function could then print out all available commands.
There is no direct way to achieve this, but you can use an array to maintain your choices:
# Define your choices - be sure not to change their order later; only
# *append* new ones.
choices=( foo bar baz )
# Make the array elements the case branches *in order*.
case "$1" in
"${choices[0]}")
echo 'do foo'
;;
"${choices[1]}")
echo 'do bar'
;;
"${choices[2]}")
echo 'do baz'
;;
*)
echo "All choices: ${choices[#]}"
esac
This makes the branches less readable, but it's a manageable solution, if you maintain your array carefully.
Note how the branch conditions are enclosed in "..." so as to prevent the shell from interpreting the values as (glob-like) patterns.
That said, as chepner points out, perhaps you want to define your choices as patterns to match variations of a string:
In that event:
Define the pattern with quotes in the choices array; e.g., choices=( foo bar baz 'h*' )
Reference it unquoted in the case branch; e.g., ${choices[3]})
bash does not give you access to the tokens it parses and does not save case strings (which can be glob expressions as well).
Unfortunately, that means you will not be able to DRY your code in the way you were hoping.
After a while of hacking together several Bash 4 features I've got this one.
#!/bin/bash
set -euo pipefail
# create coprocess with 2 descriptors so we can read and write to them
coproc CAT { cat ; }
# creme de la creme of this solution - use function to both collect and select elements
function element {
echo "$1" >&${CAT[1]}
echo "$1"
}
case "$1" in
$(element A))
echo "Your choice is: A"
;;
$(element B))
echo "Your choice is: B"
;;
*)
echo "Your choice is not present in available options: $1"
# close writing descriptor
exec {CAT[1]}>&-
#read colected options into an array
mapfile -t OPTIONS <&${CAT[0]}
echo "Available options are: [ ${OPTIONS[#]} ]"
;;
esac
Output:
Your choice is not present in available options: C
Available options are: [ A B ]
There are 2 parts for this solution:
coproc - which creates subprocess for reading and writing from subshell
function element which both writes into descriptors of coproc subrocess and returns it's argument so we can use it inside case ... esac
If handling all options should be done outside of case then you can use ;;& feature of Bash 4 case statement which forces checking every statement inside case (usually - i.e. ;; - it stops after first match). This checking is needed so we can collect all options into an array later
There is probably a lot of reasons not to use this (limits of data which can be safely stored in descriptor without reading them being one of those) and I welcome all comments which can make this solution better.
You could have your script inspect itself:
#!/bin/bash
case_start=$(("$LINENO" + 2)) #store line number of start
case "$1" in
simple_case) echo "this is easy";;
--tricky)
echo "This is a tricky"
(echo "Multiline")
echo "Statement"
;;
-h|--help) echo "heeeelp me";;
-q|--quiet) ;;
*) echo "unknown option";;
esac
case_end=$(("$LINENO" - 2)) #store line number of end
# - take lines between $case_start and $case_end
# - replace newlines with spaces
# - replace ";;" with newlines
# -=> now every case statement should be on its own line
# - then filter out cases: delete everything after the first ")" including the ")" and trim blanks
cases_available=`sed -n "${case_start},${case_end}p" $0 | sed 's/#.*//' | tr '\n' ' ' | sed 's/;;/\n/g' | sed 's/).*//;s/[[:blank:]]*//'`
echo -e "cases_available:\n\n$cases_available"
this would print:
cases_available:
simple_case
--tricky
-h|--help
-q|--quiet
*
There are some pitfalls with this:
Comments or strings inside the case statement with a ";;" in it will break stuff
Can't handle nested switch case statements.
Dunno if I understood correctly your question, but you can do something like this page says to print out all the available cases as default choice:
case "$1" in
start)
start
;;
stop)
stop
;;
status)
status anacron
;;
restart)
stop
start
;;
condrestart)
if test "x`pidof anacron`" != x; then
stop
start
fi
;;
*)
echo $"Usage: $0 {start|stop|restart|condrestart|status}"
exit 1
esac
I think the best approach and also in OP regards is to actually print the switch case structure to a new file and then source it in the original file if needed.
echo "case "'"$1"'" in" > case.sh
echo " test)" >> case.sh
echo " echo "This case!"" >> case.sh
echo " ;;" >> case.sh
echo "esac" >> case.sh
chmod+x case.sh
./case.sh test
#This case!
This way you can easily use variables to build your switch / case condition.

Passing an array with spaces to a Bash function to act as its list of arguments

I am trying to use getopts to make my script be able to take command line arguments, like -s "gil.sh 123. Because it does not support command line arguments with long names, I have a function that takes the arguments, and changes every appearance of the long version (in this case --script) to the short version (-s) and only then getopts gets called.
The problem is that if it contains spaces (in this case "gil.sh 123") then I cannot get the second function to take this as an array with 2 members, in this case i get the array (-s gil.sh 123) instead of (-s "gil.sh 123") which is what I sent the function.
Here is my code:
#!/bin/bash
#change long format arguments (-- and then a long name) to short format (- and then a single letter) and puts result in $parsed_args
function parse_args()
{
m_parsed_args=("$#")
#changes long format arguments (--looong) to short format (-l) by doing this:
#res=${res/--looong/-l}
for ((i = 0; i < $#; i++)); do
m_parsed_args[i]=${m_parsed_args[i]/--script/-s}
done
}
#extracts arguments into the script's variables
function handle_args()
{
echo "in handle_args()"
echo $1
echo $2
echo $3
while getopts ":hno:dt:r:RT:c:s:" opt; do
case $opt in
s)
#user script to run at the end
m_user_script=$OPTARG
;;
\?)
print_error "Invalid option: -$OPTARG"
print_error "For a list of options run the script with -h"
exit 1
;;
:)
print_error "Option -$OPTARG requires an argument."
exit 1
;;
esac
done
}
parse_args "$#"
handle_args ${m_parsed_args[#]}
(This code is obviously shorter than the original which has many more substitutions and types of arguments, i only left one)
I call the script like this: ./tmp.sh -s "gil.sh 123" and I can see that after parse_args the variable m_parsed_args is an array with 2 members, but when I send it to handle_args the array has 3 members, so I cannot give the variable m_user_script the correct value that i want it to get ("gil.sh 123")
Why do not you use double quotes for the m_parsed_args array?
handle_args "${m_parsed_args[#]}"

Understanding parameters in a function

I found this function:
findsit()
{
OPTIND=1
local case=""
local usage="findsit: find string in files.
Usage: fstr [-i] \"pattern\" [\"filename pattern\"] "
while getopts :it opt
do
case "$opt" in
i) case="-i " ;;
*) echo "$usage"; return;;
esac
done
shift $(( $OPTIND - 1 ))
if [ "$#" -lt 1 ]; then
echo "$usage"
return;
fi
find . -type f -name "${2:-*}" -print0 | \
xargs -0 egrep --color=always -sn ${case} "$1" 2>&- | more
}
I understand the output and what it does, but there are some terms I still don't understand and find it hard to find a reference, but believe they would be useful to learn in my programming. Can anyone quickly explain them? Some don't have man pages.
local
getopts
case
shift
$#
${2:-*}
2>&-
Thank you.
local: Local variable. Let's say you had a variable called foo in your program. You call a function that also has a variable foo. Let's say the function changes the value of foo.
Try this program:
testme()
{
foo="barfoo"
echo "In function: $foo"
}
foo="bar"
echo "In program: $foo"
testme
echo "After function in program: $foo"
Notice that the value of $foo has been changed by the function even after the function has completed. By declaring local foo="barfoo" instead of just foo="barfoo", we could have prevented this from happening.
case: A case statement is a way of specifying a list of options and what you want to do with each of those options. It is sort of like an if/then/else statement.
These two are more or less equivelent:
if [[ "$foo" == "bar" ]]
then
echo "He said 'bar'!"
elif [[ "$foo" == "foo" ]]
then
echo "Don't repeat yourself!"
elif [[ "$foo" == "foobar" ]]
then
echo "Shouldn't it be 'fubar'?"
else
echo "You didn't put anything I understand"
fi
and
case $foo in
bar)
echo "He said 'bar'!"
;;
foo)
echo "Don't repeat yourself!"
;;
foobar)
echo "Shouldn't it be 'fubar'?"
;;
*)
echo "You didn't put anything I understand"
;;
esac
The ;; ends the case option. Otherwise, it'll drop down to the next one and execute those lines too. I have each option in three lines, but they're normally combined like
foobar) echo "Shouldn't it be 'fubar'?";;
shift: The command line arguments are put in the variable called $*. When you say shift, it takes the first value in that $* variable, and deletes it.
getopts: Getopts is a rather complex command. It's used to parse the value of single letter options in the $# variable (which contains the parameters and arguments from the command line). Normally, you employ getopts in a while loop and use case statement to parse the output. The format is getopts <options> var. The var is the variable that will contain each option one at a time. The specify the single letter parameters and which ones require an argument. The best way to explain it is to show you a simple example.
$#: The number of parameters/arguments on the command line.
${var:-alternative}: This says to use the value of the environment variable $var. However, if this environment variable is not set or is null, use the value alternative instead. In this program ${2:-*} is used instead. The $2 represents the second parameter of what's left in the command line parameters/arguments after everything has been shifted out due to the shift command.
2>&-: This moves Standard Error to Standard Output. Standard Error is where error messages are put. Normally, they're placed on your terminal screen just like Standard Output. However, if you redirect your output into a file, error messages are still printed to the terminal window. In this case, redirecting the output to a file will also redirect any error messages too.
Those are bash built-ins. You should read the bash man page or, for getopts, try help getopts
One at a time (it's really annoying to type on ipad hence switched to laptop):
local lets you define local variables (within the scope of a function)
getopts is a bash builtin which implements getopt-style argument processing (the -a, -b... type arguments)
case is the bash form for a switch statement. The syntax is
case: case WORD in [PATTERN [| PATTERN]...) COMMANDS ;;]... esac
shift shifts all of the arguments by 1 (so that the second argument becomes the first, third becomes second, ...) similar to perl shift. If you specify an argument, it will shift by that many indices (so shift 2 will assign $3 -> $1, $4 -> $2, ...)
$# is the number of arguments passed to the function
${2:-*} is a default argument form. Basically, it looks at the second argument ($2 is the second arg) and if it is not assigned, it will replace it with *.
2>&- is output redirection (in this case, for standard error)

bash script parameters [duplicate]

This question already has answers here:
How do I parse command line arguments in Bash?
(40 answers)
Closed 6 years ago.
I need to write a bash script, and would like it to parse unordered parameters of the format:
scriptname --param1 <string> --param2 <string> --param3 <date>
Is there a simple way to accomplish this, or am I pretty much stuck with $1, $2, $3?
You want getopts.
while [[ $1 = -* ]]; do
arg=$1; shift # shift the found arg away.
case $arg in
--foo)
do_foo "$1"
shift # foo takes an arg, needs an extra shift
;;
--bar)
do_bar # bar takes no arg, doesn't need an extra shift
;;
esac
done
A nice example of how to implement short & long switches side by side is mcurl:
http://www.goforlinux.de/scripts/mcurl/
Bash has a getops function, as mentioned here before, that might solve your problems.
If you need anything more sophisticated, bash also supports positional parameters (ordered $1 ... $9, and then ${10} .... ${n}), you'll have to come up with your own logic to handle this input. One easy way to go is to put a switch/case inside of a for loop, iterating over the parameters. You can use either one of the two special bash vars that handle the input: $* or $#.
#!/bin/bash
# Parse the command-line arguments
while [ "$#" -gt "0" ]; do
case "$1" in
-p1|--param1)
PARAM1="$2"
shift 2
;;
-p2|--param2)
PARAM2="$2"
shift 2
;;
-p3|--param3)
PARAM3="$2"
shift 2
;;
-*|--*)
# Unknown option found
echo "Unknown option $1."
exit 1
;;
*)
CMD="$1"
break
;;
esac
done
echo "param1: $PARAM1, param2: $PARAM2, param3: $PARAM3, cmd: $CMD"
When I execute this:
./<my-script> --param2 my-param-2 --param1 myparam1 --param3 param-3 my-command
it outputs what you expect:
param1: myparam1, param2: my-param-2, param3: param-3, cmd: my-command

Resources