Can I use functions as tests in a Bash case statement? - bash

can I use a function instead of regular patterns, strings etc. in a bash case statement like this?
#!/bin/bash
var="1"
is_one () {
[$var -eq 1]
}
case $var in
is_one)
"var is one"
;;
*)
"var is not one"
break
;;
esac

You can use functions in statements where could also use programs like test/[, that is while, until, and if. The case statement however allows only patterns, no programs/functions.
Either use
if is_one; then
echo "var is one"
else
echo "var is not one"
done
or
case $var in
1) echo "var is one" ;;
*) echo "var is not one" ;;
esac

Of course you can use a function, but the function should print the pattern you want to match as a glob. That's what case operates on.
is_one () {
echo 1
}
case $var in
"$(is_one)") echo 'It matched!';;
*) echo ':-(';;
esac
Perhaps a better design if you really want to use a function is to write a one which (doesn't contain syntax errors and) returns a non-zero exit code on failure. In this particular case, encapsulating it in a function doesn't really make much sense, but of course you can:
is_one () {
[ "$1" -eq 1 ]
}
if is_one "$var"; then
echo 'It matched!"
else
echo ':-('
fi
Notice the non-optional spaces inside [...] and also the quoting, and the use of a parameter instead of hard-coding a global variable.
Furthermore, note that -eq performs numeric comparisons, and will print an unnerving error message if $var is not numeric. Perhaps you would prefer to use [ "$1" = "1" ] which performs a simple string comparison. This is also somewhat closer in semantics to what case does.

The main difference is that if... elif... distinguishes its alternatives based on the exit codes of the commands being executed, while case.... bases it on string values. Your function, when invoked, exhibits its result only via its exit code.
If you want to use your function with a case, you need to turn this exit code into a string, for instance like this:
is_one
var=$? # Store exit code as a string into var
case $var in
1) echo "exit code is one" ;;
101) echo "exit code is onehundredandone" ;;
*) echo "I do not care what the exit code is" ;;
esac

Related

Validate bash script arguments

I am trying to do something like this to make a script to perform backups if they have failed. I am taking in the environment as argument to the script.
The one thing i am unsure on how to do is that i want to verify $1 to only include some predefined values. The predefined values should be something like tst, prd, qa, rpt. Anyone?
#!/bin/bash
ENVIRONMENT=$1
BACKUPDATE=$(date +"%d_%m_%Y")
BACKUPFILE="$ENVIRONMENT".backup."$BACKUPDATE".tar.gz
if [ $1 == "" ]
then
echo "No environment specified"
exit
elif [ -f "$BACKUPFILE" ]; then
echo "The file '$BACKUPFILE' exists."
else
echo "The file '$BACKUPFILE' in not found."
exec touch "$BACKUPFILE"
fi
You can use case:
case "$1" in
tst) echo "Backing up Test style" ;;
prd)
echo "Production backup"
/etc/init.d/myservice stop
tar czf ...
/etc/init.d/myservice start
;;
qa) echo "Quality skipped" ;;
rpt)
echo "Different type of backup"
echo "This could be another processing"
...
;;
*)
echo "Unknown backup type"
exit 2
;;
esac
Note the double ;; to end each case, and the convenient use of pattern matching.
Edit: following your comment and #CharlesDuffy suggestion, if you want to have all valid options in an array and test your value against any of them (hence having the same piece of code for all valid values), you can use an associative array:
declare -A valids=(["tst"]=1 ["prd"]=1 ["qa"]=1 ["rpt"]=1)
if [[ -z ${valids[$1]} ]] ; then
echo "Invalid parameter value"
# Any other processing here ...
exit 1
fi
# Here your parameter is valid, proceed with processing ...
This works by having a value (here 1 but it could be anything else in that case) assigned to every valid parameter. So any invalid parameter will be null and the -z test will trigger.
Credits go to him.
Depending on how many different values you have, what about a case statement? It even allows for globbing.
case $1 in
(John) printf "Likes Yoko\n";;
(Paul) printf "Likes to write songs\n";;
(George) printf "Harrison\n";;
(Ringo) printf "Da drumma\n";;
(*) printf "Management, perhaps?\n";;
esac
On another note, if you can you should avoid unportable bashisms like the [[ test operator (and use [ if you can, e.g. if [ "$1" = "John" ]; then ...; fi.)

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.

bash script case statement needs to detect specific arguments

I have to write this script where it will display each entry that is entered in on its own line and separate each line with "*****". I've already got that down for the most part but now I need it to detect when the word "TestError" and/or "now" is entered in as an argument. The way it is setup right now it will detect those words correctly if they are the first argument on the line, I'm just not sure how to set it up where it will detect the word regardless of which argument it is on the line. Also I need help with the *? case where I need it to say "Do not know what to do with " for every other argument that is not "TestError" or "now", at the moment it will do it for the first argument but not the rest.
Would it work the way it is right now? Or would I have to use only the *? and * cases and just put an if/then/else/fi statement in the *? case in order to find the "TestError" "now" and any other argument.
# template.sh
function usage
{
echo "usage: $0 arguments ..."
if [ $# -eq 1 ]
then echo "ERROR: $1"
fi
}
# Script starts after this line.
case $1 in
TestError)
usage $*
;;
now)
time=$(date +%X)
echo "It is now $time"
;;
*?)
echo "My Name"
date
echo
usage
printf "%s\n*****\n" "Do not know what to do with " "$#"
;;
*)
usage
;;
esac
You'll need to loop over the arguments, executing the case statement for each one.
for arg in "$#"; do
case $arg in
TestError)
usage $*
;;
now)
time=$(date +%X)
echo "It is now $time"
;;
*?)
echo "My Name"
date
echo
usage
printf "%s\n*****\n" "Do not know what to do with " "$#"
;;
*)
usage
;;
esac
done
* and *? will match the same strings. Did you mean to match the ? literally (*\?)?

Linux Regular Expression

I'm working with shell scripting in Linux. I want to check if the value of MAX_ARCHIVE_AGE is numeric or not. My code is like this:
MAX_ARCHIVE_AGE = "50"
expr="*[0-9]*"
if test -z "$MAX_ARCHIVE_AGE";
then
echo "MAX_ARCHIVE_AGE variable is missing or not initiated"
else
if [ "$MAX_ARCHIVE_AGE" != $expr ]
then
echo "$MAX_ARCHIVE_AGE is not a valid value"
fi
fi
I want to match the value of MAX_ARCHIVE_AGE with my expr. Please help.
For POSIX compatibility, look at case. I also find it more elegant than the corresponding if construct, but the syntax may seem a bit odd when you first see it.
case $MAX_ARCHIVE_AGE in
'' ) echo "empty" >&2 ;;
*[!0-9]* ) echo "not a number" >&2 ;;
esac
By the way, notice the redirection of error messages to standard error with >&2.
Your expr will match anything that contains any digits; it's better to check if it contains only digits, or conversely, to check if it contains any non-digits. To do that, you can write:
if ! [[ "$MAX_ARCHIVE_AGE" ]] ; then
echo "MAX_ARCHIVE_AGE is blank or uninitialized" >&2
elif [[ "$MAX_ARCHIVE_AGE" == *[^0-9]* ]] ; then
echo "$MAX_ARCHIVE_AGE is not a valid value" >&2
fi
Also, note that you would initialize MAX_ARCHIVE_AGE by writing e.g. MAX_ARCHIVE_AGE=50 (no spaces), not MAX_ARCHIVE_AGE = 50. The latter tries to run a program called MAX_ARCHIVE_AGE with the arguments = and 50.

Exit a bash switch statement

I've written a menu driven bash script that uses a switch case inside of a while loop to perform the various menu options. Everything works just fine. Now I'm trying to improve the program by performing error testing on the user input, but I cannot seem to make it work...
The problem is I don't know how to properly break out of a switch statement, without breaking out of the while loop (so the user can try again).
# repeat command line indefinitely until user quits
while [ "$done" != "true" ]
do
# display menu options to user
echo "Command Menu" # I cut out the menu options for brevity....
# prompt user to enter command
echo "Please select a letter:"
read option
# switch case for menu commands, accept both upper and lower case
case "$option" in
# sample case statement
a|A) echo "Choose a month"
read monthVal
if [ "$monthVal" -lt 13 ]
then
cal "$monthVal"
else
break # THIS DOES NOT WORK. BREAKS WHILE LOOP, NOT SWITCH!
fi
;;
q|Q) done="true" #ends while loop
;;
*) echo "Invalid option, choose again..."
;;
esac
done
exit 0
The program works fine when the user enters a valid month value, but if they enter a number higher than 13, instead of breaking the switch statement and repeating the loop again, the program breaks both the switch and while loop and stops running.
Hitting ;; will terminate the case statement. Try not doing anything at all:
a|A) echo "Choose a month"
read monthVal
if [ "$monthVal" -lt 13 ]
then
cal "$monthVal"
fi
;;
Move the body of that case into a function, and you can return from the function at will.
do_A_stuff() {
echo "Choose a month"
read monthVal
if [ "$monthVal" -lt 13 ]
then
cal "$monthVal"
else
return
fi
further tests ...
}
Then
case $whatever in
a|A) do_A_stuff ;;
I think what you mean to do with break is "quit this case statement and restart the while loop". However, case ... esac isn't a control-flow statement (although it may smell like one), and doesn't pay attention to break.
Try changing break into continue, which will send control back to the start of the while loop.
This should do the trick: Wrap the code in a one-trip for-loop:
#! /bin/bash
case foo in
bar)
echo "Should never get here."
;;
foo)
for just in this_once ; do
echo "Top half only."
if ! test foo = bar; then break; fi
echo "Bottom half -- should never get here."
done
;;
*)
echo "Should never get here either."
;;
esac
In your example, there is no point in breaking, you can omit the else break statement altogether.
The problem occurs when there is code that runs after the point where you would have broken. You'd want to write something like that
case $v in
a) if [ $x ]; then bla; else break; fi;
some more stuff ;;
b) blablabla ;;
What I usually do (because creating a function is such a hassle with copy pasting, and mostly it breaks the flow of the program when you read it to have a function somewhere else) is to use a break variable (which you can call brake for fun when you have a lame sense of humor like me) and enclose "some more stuff" in an if statement
case $v in
a) if [ $x ]; then bla; else brake="whatever that's not an empty string"; fi;
if [ -z "$brake" ];then some more stuff; brake=""; fi;;
#don't forget to clear brake if you may come back here later.
b) blablabla ;;
esac

Resources