Understanding parameters in a function - bash

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)

Related

getopts in bash scripts

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.

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 ?.

Simplify Dynamic Menu in Bash

I'm working on a custom bash script (with which I have very little experience) and I'm looking for some help with a menu function. My script had several different menus accomplishing different functions so I decided to create one menu function that could be called and customized with set variables. It's unclean and ugly, but I've learned a lot making it. Can anyone shed some light on where I could have simplified or done things differently or more cleanly? Thanks in advance!
#!/bin/bash
# set colors
red='tput setaf 1'
blue='tput setaf 4'
green='tput setaf 2'
bold='tput bold'
normal='tput sgr0'
# define function to reset screen to title
function reset {
clear
$bold
$blue
echo -e "lineage build script\n\n"
$normal
}
function title {
reset
$bold
echo -e "$1\n"
$normal
}
# function to create menu
# menu "<title(required)>" "<question(required)>" <all(required)> "<option1>"...
# <title> - Page title
# <question> - Question to be posed before the menu
# <all> - Whether or not to present a menu choice for "All"
# <option#> - List of options to present to the user
function menu {
# set input at 255 (max error level for return function later)
input=255
# check input to see if it's acceptable (within presented range and a number)
while [[ $input -ge $(($counter+1)) ]] || [[ $((input)) != $input ]]; do
# a call to a previously defind function establishing consistent page feel.
title "$1"
echo -e "$2"
# present warning if input was invalid
if ! [[ $input =~ "255" ]]; then
$red
echo -e "Invalid entry. Please try again.\n"
$normal
else
echo -e "\n"
fi
counter=0
# present list items for each variable passed to function starting at position 4
# in order to skip the first 3 (title, question, all)
for i in "${#:4}"; do
counter=$(($counter+1))
echo -e "$counter) $i"
done
# present "All" option if variable 3 is any form of yes
if [[ $3 =~ (y|yes|Y|YES) ]]; then $green; counter=$(($counter+1)); echo -e "$counter) All"; $normal; fi
# present "Exit" option
$red; counter=$(($counter+1)); echo -e "$counter) Exit\n"; $normal
# gather input
read -N 1 -p "[1-$counter]: " input
done
# bail if exit was chosen
if [[ $input == $counter ]]; then $red; echo -e "\nExit"; exit; fi
# pass choice back to script as return code
return $input
}
# call the menu function for testing
menu "Main Menu" "What would you like to do?" y "option 1" "option 2" "option 3" "option 4" "option 5"
#echo return code just to verify function
echo -e "\nYou chose $?"
Let's see if this helps in anyway (in retro). Not only shell scripting related.
Naming
The reset function is a verb, where as title is not. But it could be, indicating what it does and that it's not a variable.
Printing
You consistently used echo -e for printing. As Charles Duffy pointed out in a comment below, you are sacrificing POSIX compatibility by using -e.
It is not possible to use echo portably across all POSIX systems unless both -n (as the first argument) and escape sequences are omitted.
The printf utility can be used portably to emulate any of the traditional behaviors of the echo utility as follows (assuming that IFS has its standard value or is unset)
http://pubs.opengroup.org/onlinepubs/009696799/utilities/echo.html
Whatever actual way you will use for printing: how about making that a function? If you have to change, it's one place to go. Hopefully this won't lead you to a big list of overloaded functions (for the various printing options).
Local
One reason I liked your program is that you kept variables where they belong, locally to the functions. Maybe you could step that up by actually denoting (where it makes sense) them local.
local [option] name[=value]
https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html
Shift
To continue the previous point I usually prefer to store / move the ordered parameters to local variables. For example title_text=$1. In the menu function it would allow you to use shift. You would process "title", "menu name" and "all" options then you end up with only the options list in the $# variable. A list of option names. Very handy if you later want to refactor some parts of the function (without breaking order) or you want to pass said list to another function to deal with the options.
shift [n]
Shift the positional parameters to the left by n. The positional
parameters from n+1 … $# are renamed to $1 … $#-n. Parameters
represented by the numbers $# to $#-n+1 are unset. n must be a
non-negative number less than or equal to $#. If n is zero or greater
than $#, the positional parameters are not changed. If n is not
supplied, it is assumed to be 1. The return status is zero unless n is
greater than $# or less than zero, non-zero otherwise.
https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html
Exit codes
I have found your way of use for the return values (exit codes) nice. It's somewhat a pity that the only situation where it's not used is the actual case when you call exit. What I mean is that without going into the menu function, I was confused for a little while why I never see "You chose" if I "exit".

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.

getopts printing help when no cmd. line argument was matched

I'm trying to use getopts in bash to parse command line arguments, but I couldn't figure out how to implement "default" action, if no argument was matched (or no cmdline argument given).
This is silghtly simplified version of what I've tried so far:
while getopts v:t:r:s:c: name;
do
case $name in
v) VALIDATE=1;;
t) TEST=1;;
r) REPORT=1;;
s) SYNC=1;;
c) CLEAR=1;;
*) print_help; exit 2;;
\?) print_help; exit 2;;
esac
done
Is there any (simple) way to make it call print_help; exit 2; on non matching input?
Looking between your question and the comments on Aditya's answer, I'd recommend the following:
[getopts]$ cat go
#!/bin/bash
function print_help { echo "Usage" >&2 ; }
while getopts vtrsc name; do
case $name in
v) VALIDATE=1;;
t) TEST=1;;
r) REPORT=1;;
s) SYNC=1;;
c) CLEAR=1;;
?) print_help; exit 2;;
esac
done
echo "OPTIND: $OPTIND"
echo ${##}
shift $((OPTIND - 1))
while (( "$#" )); do
if [[ $1 == -* ]] ; then
echo "All opts up front, please." >&2 ; print_help ; exit 2
fi
echo $1
shift
done
Since each of those are boolean flag options, you don't need (and in fact, do not want) the arguments, so we get rid of the colons. None of those characters are in IFS, so we don't need to wrap that in quotes, it will be one token for getopts anyway.
Next, we change the \? to a single ? and get rid of the *, as the * would match before the literal \?, and we might as well combine the rules into a single default match. This is a good thing, since any option specified with a - prefix should be an option, and users will expect the program to fail if they specify an option you don't expect.
getopts will parse up to the first thing that isn't an argument, and set OPTIND to that position's value. In this case, we'll shift OPTIND - 1 (since opts are 0-indexed) off the front. We'll then loop through those args by shifting them off, echoing them or failing if they start with a -.
And to test:
[getopts]$ ./go
OPTIND: 1
0
[getopts]$ ./go -t -v go go
OPTIND: 3
4
go
go
[getopts]$ ./go -d -v go go
./go: illegal option -- d
Usage
[getopts]$ ./go -t go -v go -d
OPTIND: 2
5
go
All opts up front, please.
Usage
Try the following workaround:
# Parse the arguments.
while getopts ':h?f:' opts; do
case ${opts} in
f) # Foo argument.
;;
# My other arguments.
\? | h | *) # Prints help.
grep " .)\ #" $0
exit 0
;;
esac
done
So basically -?/-h would print the parameters with comments based on its own source. Specifying : before options will print help for any other unknown argument also.
v:t:r:s:c: should be in double quotes
"v:t:r:s:c:"
Based on the script you posted, maybe you don't require all those colons :
Also you don't need *)
You need to provide a leading colon in the getopts option string if you want to enable ? to match an invalid option -- :vtrsc. Also you don't need the backslash before the ?

Resources