Associative array not unset when nullglob is set - bash

When I set nullglob in bash:
shopt -s nullglob
and then declare an associative array:
declare -A arr=( [x]=y )
I cannot unset specific keys within the array:
unset arr[x]
echo ${#arr[#]} # still 1
However, unsetting nullglob makes this operation work like I expect:
shopt -u nullglob
unset arr[x]
echo ${#arr[#]} # now it's 0; x has been removed
What's going on here? I don't see how shell globbing could be relevant to the situation. I've tested this on bash 4.4.19 and 5.0.0.

This can be explained by reference the the bash documentation (the man page), paraphrased here:
After word splitting, unless the -f option has been set, Bash scans each word for the characters '*', '?', and '['. If one of these characters appears, then the word is regarded as a pattern, and replaced with an alphabetically sorted list of filenames matching the pattern.
If no matching filenames are found, and the shell option nullglob is disabled, the word is left unchanged. If the nullglob option is set, and no matches are found, the word is removed.
In other words, nullglob affects what will happen to your arr[x] argument. It will either be left alone or removed.
You can see this effect by turning on echo-before-execute flag with set -x:
pax$ declare -A arr=( [x]=y )
pax$ shopt -s nullglob
pax$ set -x
pax$ unset arr[x]
+ unset
Note that this is the "word is removed" case. The "word is left unchanged" case is show thus:
pax$ shopt -u nullglob
+ shopt -u nullglob
pax$ unset arr[x]
+ unset 'arr[x]'
That final echoed command above also provides a clue as to how to delete an entry if you have enable nullglob. Just quote the argument to prevent expansion:
unset 'arr[x]'
This will work regardless of the nullglob setting, because of the section regarding quoting in the documentation:
Enclosing characters in single quotes preserves the literal value of each character within the quotes.

Related

How to list and sort pictures with just one number in its name?

How can I list all jpg with a number as a name in bash on a mac? I have a folder with several pictures all with numbers in its name.
However, I just want to have those pictures listed and sorted in bash with just 1 number in its name, with the following layout: 1.jpg or 30.jpg, but not 1-1.jpg or 30-1-2.jpg etc.
I tried ls $* | sort -V, but it gives me all pictures.
How can I achieve the desired result?
You don't need any third party tool for this, just enable bash extended globbing (if not enabled by default). The nullglob option allows filename patterns which match no files to expand to a null string, rather than themselves
shopt -s extglob nullglob
If you are sure, there can't be filenames containing other alphanumeric characters, then to exclude filenames containing -, all you need to do is
printf '%s\n' !(*-*).jpg
or be specific to match only filenames with digits as
printf '%s\n' [[:digit:]]!(*-*).jpg
Or wrap this over in a sub-shell to avoid setting the glob options persistent in your interactive shell.
( shopt -s extglob nullglob ; printf '%s\n' [[:digit:]]!(*-*).jpg ; ) |
sort -V
As to why your attempt didn't work, ls $* can never work, because $* is a special bash shell variable created by concatenating all the command line arguments passed and joined by the value of IFS. Were you trying to pass * as the argument and process $* inside a function/script and sort on the list returned?
Use Bash's built-in extended globbing patterns.
shopt -s extglob
ls +([[:digit:]]).{{[jJ][pP],[pP][nN]}[gG],{[gG],[tT]}[iI][fF]?([fF])} | sort -n
or with case insensitive globbing:
shopt -s extglob nocaseglob
ls +([[:digit:]]).{{jp,pn}g,{g,t}if?(f)} | sort -n
Or if you look only for lowercase .jpg:
shopt -s extglob
ls +([[:digit:]]).jpg | sort -n

how do I avoid case sensitivity when appending file extension to a variable

I need to avoid case sensitivity when appending a '.csv' to a variable :
shopt -s nocaseglob
date=("$1".csv)
This does not seem to be working although it works here:
shopt -s nocaseglob
files=($dirPath/*.csv)
How can I make it work so the var $1 can be read as either $1.csv or $1.CSV?
nocaseglob works only for globs (i.e. *, ?, []).
"$1".csv is not a glob and therefore not affected by the setting.
You can turn your string into a glob by using a character class that matches exactly one letter:
shopt -s nocaseglob
date=("$1".cs[v])
This works only because you know a literal part of your string. If you wanted do ("$1") then you had to rely on extended globs:
shopt -s nocaseglob
shopt -s extglob
date=("$1"?())
Looks like
shopt -s nocasematch
Works.

getops $OPTARG is empty if flag value contains brackets

When I pass a flag containing [...] to my bash script, getops gives me an empty string when I try to grab the value with $OPTARG.
shopt -s nullglob
while getopts ":f:" opt; do
case $opt in
f)
str=$OPTARG
;;
esac
done
echo ${str}
Running the script:
$ script.sh -f [0.0.0.0]
<blank line>
How can I get the original value back inside the script?
Short summary: Double-quote your variable references. And use shellcheck.net.
Long explanation: When you use a variable without double-quotes around it (e.g. echo ${str}), the shell tries to split its value into words, and expand anything that looks like a wildcard expression into a list of matching files. In the case of [0.0.0.0], the brackets make it a wildcard expression that'll match either the character "0" or "." (equivalent to [0.]). If you had a file named "0", it would expand to that string. With no matching file(s), it's normally left unexpanded, but with the nullglob set it expands to ... null.
Turning off nullglob solves the problem if there are no matching files, but isn't really the right way do it. I remember (but can't find right now) a question we had about a script that failed on one particular computer, and it turned out the reason was that one computer happened to have a file that matched a bracket expression in an unquoted variable's value.
The right solution is to put double-quotes around the variable reference. This tells the shell to skip word splitting and wildcard expansion. Here's an interactive example:
$ str='[0.0.0.0]' # Quotes aren't actually needed here, but they don't hurt
$ echo $str # This works without nullglob or a matching file
[0.0.0.0]
$ shopt -s nullglob
$ echo $str # This fails because of nullglob
$ shopt -u nullglob
$ touch 0
$ echo $str # This fails because of a matching file
0
$ echo "$str" # This just works, no matter whether file(s) match and/or nullglob is set
[0.0.0.0]
So in your script, simply change the last line to:
echo "${str}"
Note that double-quotes are not required in either case $opt in or str=$OPTARG because variables in those specific contexts aren't subject to word splitting or wildcard expansion. But IMO keeping track of which contexts it's safe to leave the double-quotes off is more hassle than it's worth, and you should just double-quote 'em all.
BTW, shellcheck.net is good at spotting common mistakes like this; I recommend feeding your scripts through it, since this is probably not the only place you have this problem.
Assuming that shopt -s nullglob is needed in the bigger script.
You can temporary disable shopt -s nullglob using shopt -u nullglob
shopt -s nullglob
shopt -u nullglob
while getopts ":f:" opt; do
case $opt in
f)
str=$OPTARG
;;
esac
done
echo ${str}
shopt -s nullglob

Case statement with extglob

With extglob on, I want to match a variable against
*( )x*
(In regex: /^ *x.*/)
This:
main(){
shopt -s extglob
local line=' x bar'
case "$line" in
*( )x*) ;;
*) ;;
esac
}
main "$#"
is giving me a syntax error. Either removing the extglob parentheses or moving shopt -s extglob outside of main, to the outer scope, fixes the problem.
Why? Why does the shopt -s extglob command need to be outside?
bash has to parse the function in order to create it, and since the extended glob syntax you're using would normally be invalid, it can't parse the function unless extglob is on when the function is created.
Net result: extglob has to be on both when the function is declared and when it runs. The shopt -s extglob line in the function takes care of the second requirement, but not the first.
BTW, there are some other places where this can be a problem. For example, if you have a while or for loop, bash needs to parse the entire loop before beginning to run it. So if something in the loop uses extglob, you have to enable it before the beginning of the loop.
You're missing the in keyword.
case "$var" in
*( )x*) echo yes;;
esac

bash loop over file mask

What's the proper way to do a for loop over a file mask?
E.g. if the mask doesn't expand to any file, then the loop should not run; else, it should run over all the files that it expands to.
The problem with naive approach is that if the * doesn't expand to anything, then the whole loop will run once as if the * part is an actual part of a filename (of a file that doesn't actually exist).
One way to do this is:
for f in abc*; do if [[ -f "$f" ]]; then
# Do something with "$f"
fi; done
That will also filter directories out of the list, which might or might not be what you want.
If you want to keep directories, use -e instead of -f.
Another way to do it is to set the shell option nullglob (may not be available on all bash versions). With nullglob set, patterns which don't match any filename will result in nothing instead of being unaltered.
shopt -s nullglob
for f in abc*; do
# do something with $f
done
shopt -u nullglob
(That leaves nullglob set for the duration of the for loop. You can unset it inside the loop instead of waiting for the end, at the smallish cost of executing the shopt -u on every loop.)
Use the nullglob shell option to make a wildcard that doesn't match anything expand into nothing instead of returning the wildcard itself:
shopt -s nullglob
for file in abc*
do
...
done

Resources