How to check if a value is in a list? - bash

I've got a script with a variable taken from command line parameters. I want to check if its value is one of dev, beta or prod. I've got a following code snippet:
#!/usr/bin/env bash
ENV_NAME=$1
echo "env name = $ENV_NAME"
ENVIRONMENTS=('dev','beta','prod')
if [[ $ENVIRONMENTS =~ $ENV_NAME ]]; then
echo 'correct'
exit
else
echo 'incorrect'
exit
fi
When I run my script, it doesn't matter which parameters I pass: ./script.sh beta or ./script.sh or ./script.sh whatever, I always get correct echoed. What is wrong in my script?

for i in ${ENVIRONMENTS[#]}; do
if [[ $i = $ENV_NAME ]]; then
echo "correct"
exit
fi
done
echo 'incorrect'
exit

For using bash re:
ENV_NAME=dev
ENVIRONMENTS="dev|beta|prod"
[[ $ENV_NAME =~ ^($ENVIRONMENTS)$ ]] && echo ${BASH_REMATCH[1]}
dev

Everyone's given a good suggestion already however there's another way. It's more efficient than using regex and probably more efficient than using a loop especially when having more values. The only thing is that this requires Bash 4.0 or newer.
declare -A ENVIRONMENTS=([dev]=. [beta]=. [prod]=.)
if [[ -n ${ENVIRONMENTS["$ENV_NAME"]} ]]; then
...

Put it in a function
I like to hide ugly implementation details, especially when it comes to bash.
function catPipe() # concatenate all arguments with a pipe character
{
local IFS='|'
echo "$*"
}
function matchList()
{
local needle="$1"
shift
local stack=$(catPipe "$#")
[[ "$needle" =~ ^($stack)$ ]]
}
If you don't like having a subshell, use this:
function matchList()
{
local needle="$1"
shift
IFS='|' eval 'local stack="$*"'
[[ "$needle" =~ ^($stack)$ ]]
}
Inspired by konsolebox' solution, I use the ^($var)$ syntax to avoid partial matches. First it looked like this:
function matchList()
{
local needle="$1"
shift
local stack="$#"
[[ ${stack[#]} =~ "$needle" ]] # would allow partial machtes like 'eta'
}
The function can be called with either a string, an array, or single items:
variants='dev beta prod'
varArray=(dev beta prod)
matchList prop $variants && echo 'prop: match!'
matchList beta $variants && echo 'beta: match!'
matchList beta ${varArray[#]} && echo 'beta: match!'
matchList eta alpha beta gamma && echo 'eta: match!'
# Output:
beta: match!
beta: match!
Or for the original example:
matchList $ENV_NAME ${ENVIRONMENTS[#]} && echo 'correct'
case (partial solution)
Side note: I tried to come up with a solution using case, as it would naturally fit the requirements.
It works, but I couldn't figure out if/how I can use ENVIRONMENTS in the statment.
ENV_NAME='beta'
ENVIRONMENTS='dev|beta|prod'
case $ENV_NAME in (dev|beta|prod) echo 'correct' ;; (*) echo 'incorrect' ;; esac

Related

Bash script with multiline variable

Here is my code
vmname="$1"
EXCEPTLIST="desktop-01|desktop-02|desktop-03|desktop-04"
if [[ $vmname != #(${EXCEPTLIST}) ]]; then
echo "${vmname}"
else
echo "Its in the exceptlist"
fi
The above code works perfectly but my question is , the EXCEPTLIST can be a long line, say 100 server names. In that case its hard to put all that names in one line. In that situation is there any way to make the variable EXCEPTLIST to be a multiline variable ? something like as follows:
EXCEPTLIST="desktop-01|desktop-02|desktop-03| \n
desktop-04|desktop-05|desktop-06| \n
desktop-07|desktop-08"
I am not sure but was thinking of possibilities.
Apparently I would like to know the terminology of using #(${})- Is this called variable expansion or what ? Does anyone know the documentation/explain to me about how this works in bash. ?
One can declare an array if the data/string is long/large. Use IFS and printf for the format string, something like:
#!/usr/bin/env bash
exceptlist=(
desktop-01
desktop-02
desktop-03
desktop-04
desktop-05
desktop-06
)
pattern=$(IFS='|'; printf '#(%s)' "${exceptlist[*]}")
[[ "$vmname" != $pattern ]] && echo good
In that situation is there any way to make the variable EXCEPTLIST to be a multiline variable ?
With your given input/data an array is also a best option, something like:
exceptlist=(
'desktop-01|desktop-02|desktop-03'
'desktop-04|desktop-05|desktop-06'
'desktop-07|desktop-08'
)
Check what is the value of $pattern variable one way is:
declare -p pattern
Output:
declare -- pattern="#(desktop-01|desktop-02|desktop-03|desktop-04|desktop-05|desktop-06)"
Need to test/check if $vmname is an empty string too, since it will always be true.
On a side note, don't use all upper case variables for purely internal purposes.
The $(...) is called Command Substitution.
See LESS=+'/\ *Command Substitution' man bash
In addition to what was mentioned in the comments about pattern matching
See LESS=+/'(pattern-list)' man bash
See LESS=+/' *\[\[ expression' man bash
s there any way to make the variable EXCEPTLIST to be a multiline variable ?
I see no reason to use matching. Use a bash array and just compare.
exceptlist=(
desktop-01
desktop-02
desktop-03
desktop-04
desktop-05
desktop-06
)
is_in_list() {
local i
for i in "${#:2}"; do
if [[ "$1" = "$i" ]]; then
return 0
fi
done
return 1
}
if is_in_list "$vmname" "${EXCEPTLIST[#]}"; then
echo "is in exception list ${vmname}"
fi
#(${})- Is this called variable expansion or what ? Does anyone know the documentation/explain to me about how this works in bash. ?
${var} is a variable expansion.
#(...) are just characters # ( ).
From man bash in Compund commands:
[[ expression ]]
When the == and != operators are used, the string to the right of the operator is considered a pattern and matched according to the rules
described below under Pattern Matching, as if the extglob shell option were enabled. ...
From Pattern Matching in man bash:
#(pattern-list)
Matches one of the given patterns
[[ command receives the #(a|b|c) string and then matches the arguments.
There is absolutely no need to use Bash specific regex or arrays and loop for a match, if using grep for raw string on word boundary.
The exception list can be multi-line, it will work as well:
#!/usr/bin/sh
exceptlist='
desktop-01|desktop-02|desktop-03|
deskop-04|desktop-05|desktop-06|
desktop-07|deskop-08'
if printf %s "$exceptlist" | grep -qwF "$1"; then
printf '%s is in the exceptlist\n' "$1"
fi
I wouldn't bother with multiple lines of text. This is would be just fine:
EXCEPTLIST='desktop-01|desktop-02|desktop-03|'
EXCEPTLIST+='desktop-04|desktop-05|desktop-06|'
EXCEPTLIST+='desktop-07|desktop-08'
The #(...) construct is called extended globbing pattern and what it does is an extension of what you probably already know -- wildcards:
VAR='foobar'
if [[ "$VAR" == fo?b* ]]; then
echo "Yes!"
else
echo "No!"
fi
A quick walkthrough on extended globbing examples: https://www.linuxjournal.com/content/bash-extended-globbing
#!/bin/bash
set +o posix
shopt -s extglob
vmname=$1
EXCEPTLIST=(
desktop-01 desktop-02 desktop-03
...
)
if IFS='|' eval '[[ ${vmname} == #(${EXCEPTLIST[*]}) ]]'; then
...
Here's one way to load a multiline string into a variable:
fn() {
cat <<EOF
desktop-01|desktop-02|desktop-03|
desktop-04|desktop-05|desktop-06|
desktop-07|desktop-08
EOF
}
exceptlist="$(fn)"
echo $exceptlist
As to solving your specific problem, I can think of a variety of approaches.
Solution 1, since all the desktop has the same desktop-0 prefix and only differ in the last letter, we can make use of {,} or {..} expansion as follows:
vmname="$1"
found=0
for d in desktop-{01..08}
do
if [[ "$vmname" == $d ]]; then
echo "It's in the exceptlist"
found=1
break
fi
done
if (( !found )); then
echo "Not found"
fi
Solution 2, sometimes, it is good to provide a list in a maintainable clear text list. We can use a while loop and iterate through the list
vmname="$1"
found=0
while IFS= read -r d
do
if [[ "$vmname" == $d ]]; then
echo "It's in the exceptlist"
found=1
break
fi
done <<EOF
desktop-01
desktop-02
desktop-03
desktop-04
desktop-05
desktop-06
desktop-07
desktop-08
EOF
if (( !found )); then
echo "Not found"
fi
Solution 3, we can desktop the servers using regular expressions:
vmname="$1"
if [[ "$vmname" =~ ^desktop-0[1-8]$ ]]; then
echo "It's in the exceptlist"
else
echo "Not found"
fi
Solution 4, we populate an array, then iterate through an array:
vmname="$1"
exceptlist=()
exceptlist+=(desktop-01 desktop-02 desktop-03 deskop-04)
exceptlist+=(desktop-05 desktop-06 desktop-07 deskop-08)
found=0
for d in ${exceptlist[#]}
do
if [[ "$vmname" == "$d" ]]; then
echo "It's in the exceptlist"
found=1
break;
fi
done
if (( !found )); then
echo "Not found"
fi

multi dimensional array (not real) variable name in bash 4.1.2

i want to use a manual created (fake multidimensinal) array in bash script but when using the array in conditions i want to use the array name from a variable.
Using bash version 4.1.2 so declare -n doesn't exist.
I guess my example will be more helpfull to see what i want to do:
declare -A test
test[ar,action]="dosomething"
test[bc,action2]="doelse"
test[bc,resolv]="dotest"
#works:
echo "this works: ${test[bc,action2]}"
#but if i want to use a variable name, bad substitution error
name="test"
echo "01 this works: ${$name[bc,action2]}"
#another test doesn't work also
echo "02 test2 : ${!name[bc,action2]}"
#final goal is to do something like this:
if [[ "${!name[bc,action2]}" == "doelse" ]]; then
echo "mission completed"
fi
checked other posts with "eval" but can't get it working.
also tested this and could work but i lost the index name in that way... i need that also.
all_elems_indirection="${name[#]}"
echo "works, a list of items : ${!all_elems_indirection}"
test3="${name}[$cust,buyer]"
echo "test3 works : ${!test3}"
second_elem_indirection="${name}[bc,action2]"
echo "test 3 works: ${!second_elem_indirection}"
#but when i want to loop through the indexes from the array with the linked values, it doesn't work, i lost the indexes.
for i in "${!all_elems_indirection}"; do
echo "index name: $i"
done
With eval, would you please try the following:
#!/bin/bash
declare -A test
test[bc,action2]="doelse"
name="test"
if [[ $(eval echo '$'{"$name"'[bc,action2]}') == "doelse" ]]; then
echo "mission completed"
fi
As eval allows execution of arbitrary code, we need to pay maximum
attention so that the codes, variables, and relevant files are under
full control and there is no room of alternation or injection.
It's just data. It's just text. Don't restrict yourself to Bash data structures. You can build your abstractions upon any underlying storage.
mydata_init() {
printf -v "$1" ""
}
mydata_put() {
printf -v "$1" "%s\n%s\n" "${!1}" "${*:2}"
}
mydata_get2() {
local IFS
unset IFS
while read -r a b v; do
if [[ "$a" == "$2" && "$b" == "$3" ]]; then
printf -v "$4" "%s" "$v"
return 0
fi
done <<<"${!1}"
return 1
}
mydata_init test
mydata_put test ar action dosomething
mydata_put test bc action2 doelse
mydata_put test bc resolv dotest
if mydata_get2 test bc action2 var && [[ "$var" == "doelse" ]]; then
echo "mission completed"
fi
When the built-in features of the language are not enough for you, you can: enhance the language, build your own abstractions, or use another language. Use Perl or Python, in which representing such data structures will be trivial.

Change variable named in argument to bash function [duplicate]

This question already has answers here:
Dynamic variable names in Bash
(19 answers)
How to use a variable's value as another variable's name in bash [duplicate]
(6 answers)
Closed 5 years ago.
In my bash scripts, I often prompt users for y/n answers. Since I often use this several times in a single script, I'd like to have a function that checks if the user input is some variant of Yes / No, and then cleans this answer to "y" or "n". Something like this:
yesno(){
temp=""
if [[ "$1" =~ ^([Yy](es|ES)?|[Nn][Oo]?)$ ]] ; then
temp=$(echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/es//g' | sed 's/no//g')
break
else
echo "$1 is not a valid answer."
fi
}
I then would like to use the function as follows:
while read -p "Do you want to do this? " confirm; do # Here the user types "YES"
yesno $confirm
done
if [[ $confirm == "y" ]]; then
[do something]
fi
Basically, I want to change the value of the first argument to the value of $confirm, so that when I exit the yesno function, $confirm is either "y" or "n".
I tried using set -- "$temp" within the yesnofunction, but I can't get it to work.
You could do it by outputting the new value and overwriting the variable in the caller.
yesno() {
if [[ "$1" =~ ^([Yy](es|ES)?|[Nn][Oo]?)$ ]] ; then
local answer=${1,,}
echo "${answer::1}"
else
echo "$1 is not a valid answer." >&2
echo "$1" # output the original value
return 1 # indicate failure in case the caller cares
fi
}
confirm=$(yesno "$confirm")
However, I'd recommend a more direct approach: have the function do the prompting and looping. Move all of that repeated logic inside. Then the call site is super simple.
confirm() {
local prompt=$1
local reply
while true; do
read -p "$prompt" reply
case ${reply,,} in
y*) return 0;;
n*) return 1;;
*) echo "$reply is not a valid answer." >&2;;
esac
done
}
if confirm "Do you want to do this? "; then
# Do it.
else
# Don't do it.
fi
(${reply,,} is a bash-ism that converts $reply to lowercase.)
You could use the nameref attribute of Bash (requires Bash 4.3 or newer) as follows:
#!/bin/bash
yesno () {
# Declare arg as reference to argument provided
declare -n arg=$1
local re1='(y)(es)?'
local re2='(n)o?'
# Set to empty and return if no regex matches
[[ ${arg,,} =~ $re1 ]] || [[ ${arg,,} =~ $re2 ]] || { arg= && return; }
# Assign "y" or "n" to reference
arg=${BASH_REMATCH[1]}
}
while read -p "Prompt: " confirm; do
yesno confirm
echo "$confirm"
done
A sample test run looks like this:
Prompt: YES
y
Prompt: nOoOoOo
n
Prompt: abc
Prompt:
The expressions are anchored at the start, so yessss etc. all count as well. If this is not desired, an end anchor ($) can be added.
If neither expression matches, the string is set to empty.

Bash == not found

I have a small bash script:
#!/bin/bash
if [[ "$#" == "pull" ]]
then
# stuff
elif [[ "$#" == "push" ]]
then
# stuff
else
echo "Command not recognised"
fi
it's located in /usr/bin/local and I made it executable. However whenever I run it I get script:1: == not found
Any ideas?
This is macOS if that matters.
Don't use [[, not defined by POSIX. Instead use [
Don't use ==, use =
Don't use $#, use $1
Don't use double quotes in this situation for pull and push, matter of fact don't use them at all
Don't use Bash when sh will do
Updated script:
#!/bin/sh
if [ "$1" = pull ]
then
# stuff
elif [ "$1" = push ]
then
# stuff
else
echo 'Command not recognised'
fi
Sticking with bash as your interpreter, your only issue is with your uses of "$#", which in tests like bash's [[ and POSIX's [ and test, expands to all arguments surrounded by quotes (just like "$*"). You probably want "$1" to test just the first argument.
You can also consider using a case (switch) statement:
#!/bin/bash
case "$1" in
( pull ) echo "you said pull" ;;
( push ) echo "you said push" ;;
( * ) echo "Command '$1' is not recognised" ;;
esac
(The above code will work in bash, sh, and zsh. I assume you still require bash due to other aspects of your code.)

Correct way to check for a command line flag in bash

In the middle of a script, I want to check if a given flag was passed on the command line. The following does what I want but seems ugly:
if echo $* | grep -e "--flag" -q
then
echo ">>>> Running with flag"
else
echo ">>>> Running without flag"
fi
Is there a better way?
Note: I explicitly don't want to list all the flags in a switch/getopt. (In this case any such things would become half or more of the full script. Also the bodies of the if just set a set of vars)
An alternative to what you're doing:
if [[ $* == *--flag* ]]
See also BashFAQ/035.
Note: This will also match --flags-off since it's a simple substring check.
I typically see this done with a case statement. Here's an excerpt from the git-repack script:
while test $# != 0
do
case "$1" in
-n) no_update_info=t ;;
-a) all_into_one=t ;;
-A) all_into_one=t
unpack_unreachable=--unpack-unreachable ;;
-d) remove_redundant=t ;;
-q) GIT_QUIET=t ;;
-f) no_reuse=--no-reuse-object ;;
-l) local=--local ;;
--max-pack-size|--window|--window-memory|--depth)
extra="$extra $1=$2"; shift ;;
--) shift; break;;
*) usage ;;
esac
shift
done
Note that this allows you to check for both short and long flags. Other options are built up using the extra variable in this case.
you can take the straight-forward approach, and iterate over the arguments to test each of them for equality with a given parameter (e.g. -t, --therizinosaurus).
put it into a function:
has_param() {
local term="$1"
shift
for arg; do
if [[ $arg == "$term" ]]; then
return 0
fi
done
return 1
}
… and use it as a predicate in test expressions:
if has_param '-t' "$#"; then
echo "yay!"
fi
if ! has_param '-t' "$1" "$2" "$wat"; then
echo "nay..."
fi
if you want to reject empty arguments, add an exit point at the top of the loop body:
for arg; do
if [[ -z "$arg" ]]; then
return 2
fi
# ...
this is very readable, and will not give you false positives, like pattern matching or regex matching will.
it will also allow placing flags at arbitrary positions, for example, you can put -h at the end of the command line (not going into whether it's good or bad).
but, the more i thought about it, the more something bothered me.
with a function, you can take any implementation (e.g. getopts), and reuse it. encapsulation rulez!
but even with commands, this strength can become a flaw. if you'll be using it again and again, you'll be parsing all the arguments each time.
my tendency is to favor reuse, but i have to be aware of the implications. the opposed approach would be to parse these arguments once at the script top, as you dreaded, and avoid the repeated parsing.
you can still encapsulate that switch case, which can be as big as you decide (you don't have to list all the options).
You can use the getopt keyword in bash.
From http://aplawrence.com/Unix/getopts.html:
getopt
This is a standalone executable that has been around a long time.
Older versions lack the ability to handle quoted arguments (foo a "this
won't work" c) and the versions that can, do so clumsily. If you are
running a recent Linux version, your "getopt" can do that; SCO OSR5,
Mac OS X 10.2.6 and FreeBSD 4.4 has an older version that does not.
The simple use of "getopt" is shown in this mini-script:
#!/bin/bash
echo "Before getopt"
for i
do
echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
echo "-->$i"
done
I've made small changes to the answer of Eliran Malka:
This function can evaluate different parameter synonyms, like "-q" and "--quick". Also, it does not use return 0/1 but an echo to return a non-null value when the parameter is found:
function has_param() {
local terms="$1"
shift
for term in $terms; do
for arg; do
if [[ $arg == "$term" ]]; then
echo "yes"
fi
done
done
}
# Same usage:
# Assign result to a variable.
FLAG_QUICK=$(has_param "-q --quick" "$#") # "yes" or ""
# Test in a condition using the nonzero-length-test to detect "yes" response.
if [[ -n $(has_param "-h --help" "$#") ]]; then;
echo "Need help?"
fi
# Check, is a flag is NOT set by using the zero-length test.
if [[ -z $(has_param "-f --flag" "$#") ]]; then
echo "FLAG NOT SET"
fi
The modification of Dennis Williamson's answer with additional example for a argument in the short form.
if [[ \ $*\ == *\ --flag\ * ]] || [[ \ $*\ == *\ -f\ * ]]
It solves the problem of false positive matching --flags-off and even --another--flag (more popular such case for an one-dashed arguments: --one-more-flag for *-f*).
\ (backslash + space) means space for expressions inside [[ ]]. Putting spaces around $* allows to be sure that the arguments contacts neither line's start nor line's end, they contacts only spaces. And now the target flag surrounded by spaces can be searched in the line with arguments.
if [ "$1" == "-n" ]; then
echo "Flag set";
fi
Here is a variation on the most voted answer that won't pick up false positives
if [[ " $* " == *" -r "* ]]; then
Not an alternative, but an improvement, though.
if echo $* | grep -e "\b--flag\b" -q
Looking for word boundaries will make sure to really get the option --flag and neither --flagstaff nor --not-really--flag

Resources