bash case statement quoting behaves as documented, but please explain - bash

c.f. the case example here
Specifically:
$: g=a*[b]; case $g in $g) echo 'unquoted pattern' ;; "$g") echo 'quoted pattern' ;; esac
quoted pattern
A simple string behaves exactly as expected -
$: g=a; case $g in $g) echo 'unquoted pattern' ;; "$g") echo 'quoted pattern' ;; esac
unquoted pattern
I made sure there was no matching file to glob, no shopt options muddying the water, and tested the value explicitly:
$: echo #$g# "#$g#" $g "$g"
#a*[b]# #a*[b]# a*[b] a*[b]
$: [[ $g == "$g" ]] && echo ok || echo no
ok
So why does it, in this case, choose the second option?
Shouldn't both evaluate to the same results?

The case statement uses pattern matching, not string equality, to compare the word to each pattern.
The pattern a*[b] matches any string starting with an a, with zero or more characters followed by a single b. It does not match the string a*[b], because that string ends with a ], not a b.

Related

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 use curly braces in case selector in bash?

I want to make some actions for matched lines in a case switch. And because strings are long, I wanted to use bash curly braces. But it does not work.
This code without curly braces works as expected:
for i in longstr_one longstr_two; do
case $i in
longstr_one| longstr_five)
echo matched $i
;;
*)
echo no matches of $i
;;
esac
done
And I got expected result:
matched longstr_one
no matches of longstr_two
But the following code with curly braces does not:
for i in longstr_one longstr_two; do
case $i in
longstr_{one|,five})
echo matched $i
;;
*)
echo no matches of $i
;;
esac
done
And I got incorrect result:
no matches of longstr_one
no matches of longstr_two
Why it is not working?
Is it possible to use curly braces in case selector in bash?
Since brace expansion isn't done in case patterns, you could use bash's extended glob syntax instead:
shopt -s extglob
for i in longstr_one longstr_two; do
case $i in
longstr_#(one|five) )
echo "matched $i"
;;
*)
echo "no matches of $i"
;;
esac
done
The syntax #(this|that|theother|...) matches any one of the subpatterns.
From bash manual:
case
The syntax of the case command is:
case word in
[ [(] pattern [| pattern]…) command-list ;;]…
esac
...
... Each pattern undergoes tilde expansion, parameter expansion, command substitution, and arithmetic expansion.
...
That means that brace expansion is not performed on case's pattern, so it is not possible to use it here.
Instead of matching on the entire value of i, just match on the portion remaining after you remove the common prefix.
for i in longstr_one longstr_two; do
case ${i#longstr_} in
one|five)
echo matched $i
;;
*)
echo no matches of $i
;;
esac
done
This approach does not depend on any non-standard extensions like extglob.

How to match single character on bash script

I want to write a bash script to read an input from user and check whether the input matches a single character. Below is the code.
read letter
if [[ $letter =~ ([a-zA-Z]) ]]
then
echo correct
fi
When I input two characters it still says "correct", see below output:
$ sh tmp.sh
aa
correct
how to write regular expression to matches exactly one character?
Your regex is not using anchors hence it will match any input that has [a-zA-Z] but it will match anything that is greater than 1 in length.
Having said that you don't even need regex and can use glob pattern here which will match exactly one alphabet:
if [[ $letter == [a-zA-Z] ]]
then
echo 'correct'
fi
If you must use regex then use:
if [[ $letter =~ ^[a-zA-Z]$ ]]
then
echo 'correct'
fi
Given that you probably want to take different actions depending on the value, you may want to consider a simple case statement:
case $letter in [a-zA-Z]) echo correct;; *) : do nothing;; esac
This will make it easy change to things like
case $letter in
a) : do something for a;;
[b-m]) : do something else;;
[n-zA-Z) : ... ;;
* ) ... ;;
esac

How to detect a filename within a case statement - in unix shell?

I coded the below code and if no -l or -L option is passes to the script I need to assume (detect) whether a filename was passed as a param. The below third condition only matches if filename is one lowercase character. How can I make it flexible to match upper and lower case letters or variable length of the string?
while [ $# -gt 0 ]
do
case $1 in
-l) echo ls;;
-L) echo ls -l;;
[a-z]) echo filename;;
*) echo usage
exit 1;;
esac
shift
done
Also, how can I include a condition in that case statement that would react to empty $1?
If for example the script is called without any options or filenames.
You can match an empty string with a '') or "") case.
A file name can contain any character--even weird ones likes symbols, spaces, newlines, and control characters--so trying to figure out if you have a file name by looking for letters and numbers isn't the right way to do it. Instead you can use the [ -e filename ] test to check if a string is a valid file name.
You should, by the way, put "$1" in double quotes so your script will work if the file name does contain spaces.
case "$1" in
'') echo empty;;
-l) echo ls;;
-L) echo ls -l;;
*) if [ -e "$1" ]; then
echo filename
else
echo usage >&2 # echo to stderr
exit 1
fi;;
esac
Use getopts to parse options, then treat remaining non-option arguments however you like (such as by testing if they're a file).
you find the information in the bash man page if you search for "Pattern Matching" without the quotes.this does the trick: [a-zA-Z0-9]*)
you should probably read on about pattern matching, regular expressions and so on.
furthermore you should honour john kugelmans hint about the double quotes.
in the following code snippet you can see how to check if no parameter got passed.
#!/bin/sh
case "$1" in
[a-zA-Z0-9]*)
echo "filename"
;;
"")
echo "no data"
;;
esac
#OP, generally if you are using bash, to match case insensitivity you can use shopt and set nocasematch
$ shopt -s nocasematch
to check null in your case statement
case "$var" in
"") echo "empty value";;
esac
You are better off falling into the default case *) and there you can at least check if the file exists with [ -e "$1" ] ... if it doesn't then echo usage and exit 1.

shell matching a pattern using a case-statement which is stored inside a variable

I am trying to match a pattern with a case-statement where the pattern is stored inside a variable. Here is a minimal example:
PATTERN="foo|bar|baz|bla"
case "foo" in
${PATTERN})
printf "matched\n"
;;
*)
printf "no match\n"
;;
esac
Unfortunately the "|" seems to be escaped (interestingly "*" or "?" are not). How do I get this to work, i.e. to match "foo"? I need to store the pattern in a variable because it is constructed dynamically. This needs to work on a POSIX compatible shell.
It is possible to match a sub-string in a string without spawning a sub-process (such as grep) using the only POSIX compliant methods of sh(1p), as defined in Section 2.6.2, Parameter Expansion.
Here is a convenience function:
# Note:
# Unlike a regular expression, the separator *must* enclose the pattern;
# and it could be a multi chars.
isin() {
PATTERN=${2:?a pattern is required}
SEP=${3:-|}
[ -z "${PATTERN##*${SEP}${1}${SEP}*}" ]
}
Examples:
for needle in foo bar; do
isin "$needle" "|hello|world|foo|" && echo "found: $needle"
done
# using ";" as separator
for needle in foo bar; do
isin "$needle" ";hello;world;foo;" \; && echo "found: $needle"
done
# using the string "RS" as separator
for needle in foo bar; do
isin "$needle" "RShelloRSworldRSfooRS" RS && echo "found: $needle"
done
You can mix this solution with the case statement if you want both of the worlds:
PATTERN="|foo bar|baz|bla|"
case "$needle" in
xyz) echo "matched in a static part" ;;
*)
if [ -z "${PATTERN##*|${needle}|*}" ]; then
echo "$needle matched $PATTERN"
else
echo "not found"
fi
esac
Note
Sometimes it is good to remember you could do your whole script in awk(1p), which is also POSIX, but I believe this is for another answer.
This should work:
PATTERN="foo|bar|baz|bla"
shopt -s extglob
case "foo" in
#($(echo $PATTERN)))
printf "matched\n"
;;
*)
printf "no match\n"
;;
esac
Your pattern is in fact a list of patterns, and the separator | must be given literally. Your only option seems to be eval. However, try to avoid that if you can.
"You can't get there from here"
I love using case for pattern matching but in this situation you're running past the edge of what bourne shell is good for.
there are two hacks to solve this problem:
at the expense of a fork, you could use egrep
pattern="this|that|those"
if
echo "foo" | egrep "$pattern" > /dev/null 2>&1
then
echo "found"
else
echo "not found"
fi
You can also do this with only built-ins using a loop. Depending on the situation, this may make your code run a billion times slower, so be sure you understand what's going on with the code.
pattern="this|that|those"
IFS="|" temp_pattern="$pattern"
echo=echo
for value in $temp_pattern
do
case foo
in
"$list") echo "matched" ; echo=: ; break ;;
esac
done
$echo not matched
This is clearly a horror show, an example of how shell scripts can quickly spin out of control if you try to make do anything even a little bit off the map..
Some versions of expr (e.g. GNU) allow pattern matching with alternation.
PATTERN="foo\|bar\|baz"
VALUE="bar"
expr "$VALUE" : "$PATTERN" && echo match || echo no match
Otherwise, I'd use a tool like awk:
awk -v value="foo" -v pattern="$PATTERN" '
BEGIN {
if (value ~ pattern) {
exit 0
} else {
exit 1
}
}'
or more tersely:
awk -v v="foo" -v p="$PATTERN" 'BEGIN {exit !(v~p)}'
You can use it like this:
PATTERN="foo|bar|baz"
VALUE=oops
matches() { awk -v v="$1" -v p="$2" 'BEGIN {exit !(v~p)}'; }
if matches "$VALUE" "$PATTERN"; then
echo match
else
echo no match
fi
You can use regex matching in Bash:
PATTERN="foo|bar|baz|bla"
if [[ "foo" =~ $PATTERN ]]
then
printf "matched\n"
elif . . .
. . .
elif . . .
. . .
else
printf "no match\n"
fi
This obviates the need for escaping or anything else:
PATTERN="foo bar baz bla"
case "foo" in
${PATTERN// /|})
printf "matched\n"
;;
*)
printf "no match\n"
;;
esac

Resources