How to use patterns in a case statement? - bash

The man page says that case statements use "filename expansion pattern matching".
I usually want to have short names for some parameters, so I go:
case $1 in
req|reqs|requirements) TASK="Functional Requirements";;
met|meet|meetings) TASK="Meetings with the client";;
esac
logTimeSpentIn "$TASK"
I tried patterns like req* or me{e,}t which I understand would expand correctly to match those values in the context of filename expansion, but it doesn't work.

Brace expansion doesn't work, but *, ? and [] do. If you set shopt -s extglob then you can also use extended pattern matching:
?() - zero or one occurrences of pattern
*() - zero or more occurrences of pattern
+() - one or more occurrences of pattern
#() - one occurrence of pattern
!() - anything except the pattern
Here's an example:
shopt -s extglob
for arg in apple be cd meet o mississippi
do
# call functions based on arguments
case "$arg" in
a* ) foo;; # matches anything starting with "a"
b? ) bar;; # matches any two-character string starting with "b"
c[de] ) baz;; # matches "cd" or "ce"
me?(e)t ) qux;; # matches "met" or "meet"
#(a|e|i|o|u) ) fuzz;; # matches one vowel
m+(iss)?(ippi) ) fizz;; # matches "miss" or "mississippi" or others
* ) bazinga;; # catchall, matches anything not matched above
esac
done

I don't think you can use braces.
According to the Bash manual about case in Conditional Constructs.
Each pattern undergoes tilde
expansion, parameter expansion,
command substitution, and arithmetic
expansion.
Nothing about Brace Expansion unfortunately.
So you'd have to do something like this:
case $1 in
req*)
...
;;
met*|meet*)
...
;;
*)
# You should have a default one too.
esac

if and grep -Eq
arg='abc'
if echo "$arg" | grep -Eq 'a.c|d.*'; then
echo 'first'
elif echo "$arg" | grep -Eq 'a{2,3}'; then
echo 'second'
fi
where:
-q prevents grep from producing output, it just produces the exit status
-E enables extended regular expressions
I like this because:
it is POSIX 7
it supports extended regular expressions, unlike POSIX case
the syntax is less clunky than case statements when there are few cases
One downside is that this is likely slower than case since it calls an external grep program, but I tend to consider performance last when using Bash.
case is POSIX 7
Bash appears to follow POSIX by default without shopt as mentioned by https://stackoverflow.com/a/4555979/895245
Here is the quote: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_01 section "Case Conditional Construct":
The conditional construct case shall execute the compound-list corresponding to the first one of several patterns (see Pattern Matching Notation) [...] Multiple patterns with the same compound-list shall be delimited by the '|' symbol. [...]
The format for the case construct is as follows:
case word in
[(] pattern1 ) compound-list ;;
[[(] pattern[ | pattern] ... ) compound-list ;;] ...
[[(] pattern[ | pattern] ... ) compound-list]
esac
and then http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13 section "2.13. Pattern Matching Notation" only mentions ?, * and [].

Related

Why doesn't ${*// /*} work to replace blanks with * in command line arguments?

I want my script to perform the product of all its integer arguments. Instead of performing a loop I tried to replace blanks with * and then compute the operation. But I got the following result which I don't understand:
#!/bin/bash
# product.sh
echo $(( ${*// /*} )) # syntax error with ./product.sh 2 3 4
args=$*
echo $(( ${args// /*} )) # ./product.sh 2 3 4 => outputs 24
How is it that the first one produces an error while using an intermediate variable works fine?
How is it that the first one produces an error:
From the Bash Reference Manual:
If parameter is ‘#’ or ‘*’, the substitution operation is applied to each positional parameter in turn
(emphasis mine)
That is, the expression ${*// /*} replaces spaces inside positional parameters, not the spaces separating positional parameters. That expression expands to 2 3 4 (which gives a syntax error when used in an arithmetic context), since the parameters itself don't contain a space. Try with
./product '2 ' '3 ' 4
and you will see the difference.
In your example, the value $* does not actually contain any literal spaces, so ${*// /*} does not do anything.
If it did, those asterisks would be subject to wildcard expansion, so the idea of performing a substitution would seem to be rather brittle even if it worked.
I would simply create a function to process the arguments, instead of rely on trickery with substitutions -- these tend to have icky corner cases when one of the arguments is a variable or etc.
mul () {
case $# in
[01]) echo "$#";;
*) local n=$1; shift; echo $((n * $(mul "$#")));;
esac
}
You may utilize IFS:
#!/bin/bash
# product.sh
# set IFS to *
IFS='*'
# use IFS in output
echo "$*"
# perform arithmetic
echo "$(( $* ))";
Output:
2*3*4
24
Or use printf, like this:
echo $(( $(printf '%s*' $*)1 ))

Why does my variable work in a test statement, but not in a case statement until I use echo to read it into itself?

I have a line with a comment. I use parameter substitution to condition the line into a variable "source". A test statement shows that the value of source is "Simple:", but the case statement can't match it. If I use command substitution to "source=$(echo $source)", test says it matches, like before, and the case statement works now. Am I missing something fundamental, should I not use parameter substitution to do this, or is this weird? Bash version: GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu). Thanks for taking a look.
Piping the line to sed with echo works as expected. If no parameter substitution is performed on a variable, case works as expected. Example: line="Simple:" and case $line in ... no issues.
#!/bin/bash
line="Simple: #comment and space to be removed"
source=${line//#*}
source=${source//^[[:space:]]*}
source=${source//*[[:space:]]$}
[[ $source =~ 'Simple:' ]] && echo -e "\n1st test match" || echo -e "\nno 1st test match"
case $source in
'Simple:')
ops="Simple"
echo -e "\n1st try case match. Ops is $ops"
;;
*)
echo -e "\nno natch in 1st case"
;;
esac
source=$(echo $source)
[[ $source =~ 'Simple:' ]] && echo -e "\n2nd test match" || echo -e "\nno 2nd test match"
case $source in
'Simple:')
ops="Simple"
echo -e "\n2nd try case match. Ops is $ops"
;;
*)
echo -e "\nno match 2nd case"
;;
esac
I expect "Simple:" would match in the first case statement, but it doesn't until I run "source=$(echo $source)".
Quoting from man bash:
${parameter/pattern/string}
Pattern substitution. The pattern is expanded to produce a pattern just as in pathname expansion, Parameter is expanded and the longest match of pattern against its value is replaced with string. ...
That means, these lines:
source=${source//^[[:space:]]*}
source=${source//*[[:space:]]$}
do nothing at all, ^ and $ doesn't work in pathname expansion; the pattern is not a regex. source=$(echo $source) makes it work because since $source is not in double-quotes, its value undergoes word splitting and the space at the end gets lost.
The proper way of doing this using parameter expansions is:
source=${line%%#*}
source=${source#${source%%[^[:space:]]*}}
source=${source%${source##*[^[:space:]]}}

Is it possible to generate dynamic patterns for case statement in Bash?

For example,
a="1|2|3"
b=3
case $b in
$a )
echo in
;;
* )
echo out
;;
*)
esac
I'd like $a to be expanded as 1|2|3. But seems it cannot work as expected. Thanks for any suggestion.
The problem is that | is not part of the pattern, but part of the case statement's syntax that separates two patterns. The following would work:
foo=3
b1=1
b2=2
b3=3
case $foo in
$b1|$b2|$b3) echo match ;;
esac
The | needs to be visible to the parser before parameter expansion occurs to act as a pattern separator. If the | is produced by a parameter expansion, it is treated as a literal character to match as part of a pattern.

Case insensitive comparision in If condition

I have this csv file and i need to count the number of rows which satisfies the condition that the row entry is betwen a certain year range and the artist_name matches the name argument. But the string matching should be case insensitive. How do i achieve that in the if loop..
I am a beginner, so please bear with me
#!/bin/bash
file="$1"
artist="$2"
from_year="$(($3-1))"
to_year="$(($4+1))"
count=0
while IFS="," read arr1 arr2 arr3 arr4 arr5 arr6 arr7 arr8 arr9 arr10 arr11 ; do
if [[ $arr11 -gt $from_year ]] && [[ $arr11 -lt $to_year ]] && [[ $arr7 =~ $artist ]]; then
count=$((count+1))
fi
done < "$file"
echo $count
The $arr7 =~ $artist part is where i need to make the modification
Bash has a builtin method for converting strings to lower case. Once they are both lower case, you can compare them for equality. For example:
$ arr7="Rolling Stones"
$ artist="rolling stoneS"
$ [ "${arr7,,}" = "${artist,,}" ] && echo "Matches!"
Matches!
$ [[ ${arr7,,} =~ ${artist,,} ]] && echo "Matches!"
Matches!
Details
${parameter,,} converts all characters in a string to lower case. If you wanted to convert to upper case, use ${parameter^^}. If you want to convert just some of the characters, use ${parameter,,pattern} where only those characters matching pattern are changed. Still more details on this are documented by manbash`:
${parameter^pattern}
${parameter^^pattern}
${parameter,pattern}
${parameter,,pattern}
Case modification. This expansion modifies the case of alphabetic characters in parameter. The pattern is expanded to
produce a pattern just
as in pathname expansion. The ^ operator converts lowercase letters matching pattern to uppercase; the , operator
converts matching uppercase
letters to lowercase. The ^^ and ,, expansions convert each matched character in the expanded value; the ^ and , expansions
match and convert
only the first character in the expanded value. If pattern is omitted, it is treated like a ?, which matches every
character. If parameter
is # or *, the case modification operation is applied to each positional parameter in turn, and the expansion is the
resultant list. If
parameter is an array variable subscripted with # or *, the case modification operation is applied to each member of the array
in turn, and
the expansion is the resultant list.
Compatibility
These case modification methods require bash version 4 (released on 2009-Feb-20) or better.
The bash case-transformations (${var,,} and ${var^^}) were introduced (some time ago) in bash version 4. However, if you are using Mac OS X, you most likely have bash v3.2 which doesn't implement case-transformation natively.
In that case, you can do lower-cased comparison less efficiently and with a lot more typing using tr:
if [[ $(tr "[:upper:]" "[:lower:]" <<<"$arr7") = $(tr "[:upper:]" "[:lower:]" <<<"$artist") ]]; then
# ...
fi
By the way, =~ does a regular expression comparison, not a string comparison. You almost certainly wanted =. Also, instead of [[ $x -lt $y ]] you can use an arithmetic compound command: (( x < y )). (In arithmetic expansions, it is not necessary to use $ to indicate variables.)
Use shopt -s nocasematch
demo
#!/bin/bash
words=(Cat dog mouse cattle scatter)
#Print words from list that match pat
print_matches()
{
pat=$1
echo "Pattern to match is '$pat'"
for w in "${words[#]}"
do
[[ $w =~ $pat ]] && echo "$w"
done
echo
}
echo -e "Wordlist: (${words[#]})\n"
echo "Normal matching"
print_matches 'cat'
print_matches 'Cat'
echo -e "-------------------\n"
echo "Case-insensitive matching"
shopt -s nocasematch
print_matches 'cat'
print_matches 'CAT'
echo -e "-------------------\n"
echo "Back to normal matching"
shopt -u nocasematch
print_matches 'cat'
output
Wordlist: (Cat dog mouse cattle scatter)
Normal matching
Pattern to match is 'cat'
cattle
scatter
Pattern to match is 'Cat'
Cat
-------------------
Case-insensitive matching
Pattern to match is 'cat'
Cat
cattle
scatter
Pattern to match is 'CAT'
Cat
cattle
scatter
-------------------
Back to normal matching
Pattern to match is 'cat'
cattle
scatter

Why doesn't [[ ... ]] work when script called with sh, while [ ... ] works always?

Script test.sh:
#!/bin/bash
if [[ $# -eq 0 ]]; then
echo "no arg"
else
echo "have arg"
fi
When I ran it as
./test.sh
it said "no arg", which was expected, but if I run it as
sh ./test.sh
it prints "have arg", but it you print $#, it's zero in both cases.
However, the script below
#!/bin/bash
if [ $# -eq 0 ]; then
echo "no arg"
else
echo "have arg"
fi
prints "no arg" in both cases.
Could somebody explain this? Why does [[ ... ]] interpret $# differently from [ ... ]?
The explanations I've read about [[ ... ]] weren't clear enough about this.
/bin/sh is often a different shell interpreter than bash. On my ubuntu system, it's a symlink to dash. The different shells have different syntax.
[ foo ] and test foo are equivalent in both bash and dash.
[[ foo ]] is a bash expression that is similar to [ ] tests, but with some differences that are noted in man bash.
Dash does not have a [[ command, which results in an error-exitcode, and the execution of the else branch.
[[ expression ]]
Return a status of 0 or 1 depending on the evaluation of the
conditional expression expression. Expressions are composed of
the primaries described below under CONDITIONAL EXPRESSIONS.
Word splitting and pathname expansion are not performed on the
words between the [[ and ]]; tilde expansion, parameter and
variable expansion, arithmetic expansion, command substitution,
process substitution, and quote removal are performed. Condi‐
tional operators such as -f must be unquoted to be recognized as
primaries.
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. If the shell
option nocasematch is enabled, the match is performed without
regard to the case of alphabetic characters. The return value
is 0 if the string matches (==) or does not match (!=) the pat‐
tern, and 1 otherwise. Any part of the pattern may be quoted to
force it to be matched as a string.
An additional binary operator, =~, is available, with the same
precedence as == and !=. When it is used, the string to the
right of the operator is considered an extended regular expres‐
sion and matched accordingly (as in regex(3)). The return value
is 0 if the string matches the pattern, and 1 otherwise. If the
regular expression is syntactically incorrect, the conditional
expression's return value is 2. If the shell option nocasematch
is enabled, the match is performed without regard to the case of
alphabetic characters. Any part of the pattern may be quoted to
force it to be matched as a string. Substrings matched by
parenthesized subexpressions within the regular expression are
saved in the array variable BASH_REMATCH. The element of
BASH_REMATCH with index 0 is the portion of the string matching
the entire regular expression. The element of BASH_REMATCH with
index n is the portion of the string matching the nth parenthe‐
sized subexpression.
In my case (ubuntu 9.04) I also see the following error line ABOVE the "have arg" line:
./test.sh: 6: [[: not found
And indeed that's as it should be; /bin/sh is a symbolic link to 'dash' not 'bash'.

Resources