Is there an easy way to set nullglob for one glob
Asked Answered
C

5

27

In bash, if you do this:

mkdir /tmp/empty
array=(/tmp/empty/*)

you find that array now has one element, "/tmp/empty/*", not zero as you'd like. Thankfully, this can be avoided by turning on the nullglob shell option using shopt -s nullglob

But nullglob is global, and when editing an existing shell script, may break things (e.g., did someone check the exit code of ls foo* to check if there are files named starting with "foo"?). So, ideally, I'd like to turn it on only for a small scope—ideally, one filename expansion. You can turn it off again using shopt -u nullglob But of course only if it was disabled before:

old_nullglob=$(shopt -p | grep 'nullglob$')
shopt -s nullglob
array=(/tmp/empty/*)
eval "$old_nullglob"
unset -v old_nullglob

makes me think there must be a better way. The obvious "put it in a subshell" doesn't work as of course the variable assignment dies with the subshell. Other than waiting for the Austin group to import ksh93 syntax, is there?

Chayachayote answered 3/2, 2012 at 9:2 Comment(0)
U
12

With mapfile in Bash 4, you can load an array from a subshell with something like: mapfile array < <(shopt -s nullglob; for f in ./*; do echo "$f"; done). Full example:

$ shopt nullglob
nullglob        off
$ find
.
./bar baz
./qux quux
$ mapfile array < <(shopt -s nullglob; for f in ./*; do echo "$f"; done)
$ shopt nullglob
nullglob        off
$ echo ${#array[@]}
2
$ echo ${array[0]}
bar baz
$ echo ${array[1]}
qux quux
$ rm *
$ mapfile array < <(shopt -s nullglob; for f in ./*; do echo "$f"; done)
$ echo ${#array[@]}
0
  • Be sure to glob with ./* instead of a bare * when using echo to print the file name
  • Doesn't work with newline characters in the filename :( as pointed out by derobert

If you need to handle newlines in the filename, you will have to do the much more verbose:

array=()
while read -r -d $'\0'; do
    array+=("$REPLY")
done < <(shopt -s nullglob; for f in ./*; do printf "$f\0"; done)

But by this point, it may be simpler to follow the advice of one of the other answers.

Uninhibited answered 14/12, 2013 at 7:49 Comment(3)
This is pretty good. It will fail if filenames have newlines in them, but although that's allowed, it's pretty rare. As long as file names aren't under an attacker's control.Chayachayote
Yes, newlines in filenames do exist. Normally, that should only mean "find the culprit and punish him/her". Except that the only time I saw newlines in file names, the culprit was a charming elder lady...Capp
This solution keeps inserting newlines at the end of file names that don't originally contain one. If you use mapfile -t it will strip off a newline at the end of the filename if it exists.Uranous
U
18

Unset it when done:

shopt -u nullglob

And properly (i.e. storing the previous state):

shopt -u | grep -q nullglob && changed=true && shopt -s nullglob
... do whatever you want ...
[ $changed ] && shopt -u nullglob; unset changed
Uela answered 3/2, 2012 at 15:13 Comment(4)
That does avoid the eval, and I didn't realize -s and -u had that behavior (help shopt fails to mention it). I'm upvoting it for those. But honestly, its not really simpler—in terms of what you have to remember to get right, there is a lot in that first line.Chayachayote
I never said it was simpler :-) But it just do what your question asked, use nullglob without affecting other scripts started from the same session. Remembering the value of nullglob adds complexity, but it's the only way you can preserve the environment as it was.Uela
Well, the question was for a better or easier way to do it (as the second code example in the question also manages to restore the setting back to its previous state). Apparently, I didn't make that clear in the question—please suggest how to improve it.Chayachayote
I don't think you can improve that. The amount of code being called is minimal. Indeed you said an "easier" way, but your example is not doing what you wanted, i.e. not cluttering the rest of the code with your changes to nullglob. Sorry if this is not the answer you were expecting, perhaps somebody else can answer it better. Don't despair ;-)Uela
U
12

With mapfile in Bash 4, you can load an array from a subshell with something like: mapfile array < <(shopt -s nullglob; for f in ./*; do echo "$f"; done). Full example:

$ shopt nullglob
nullglob        off
$ find
.
./bar baz
./qux quux
$ mapfile array < <(shopt -s nullglob; for f in ./*; do echo "$f"; done)
$ shopt nullglob
nullglob        off
$ echo ${#array[@]}
2
$ echo ${array[0]}
bar baz
$ echo ${array[1]}
qux quux
$ rm *
$ mapfile array < <(shopt -s nullglob; for f in ./*; do echo "$f"; done)
$ echo ${#array[@]}
0
  • Be sure to glob with ./* instead of a bare * when using echo to print the file name
  • Doesn't work with newline characters in the filename :( as pointed out by derobert

If you need to handle newlines in the filename, you will have to do the much more verbose:

array=()
while read -r -d $'\0'; do
    array+=("$REPLY")
done < <(shopt -s nullglob; for f in ./*; do printf "$f\0"; done)

But by this point, it may be simpler to follow the advice of one of the other answers.

Uninhibited answered 14/12, 2013 at 7:49 Comment(3)
This is pretty good. It will fail if filenames have newlines in them, but although that's allowed, it's pretty rare. As long as file names aren't under an attacker's control.Chayachayote
Yes, newlines in filenames do exist. Normally, that should only mean "find the culprit and punish him/her". Except that the only time I saw newlines in file names, the culprit was a charming elder lady...Capp
This solution keeps inserting newlines at the end of file names that don't originally contain one. If you use mapfile -t it will strip off a newline at the end of the filename if it exists.Uranous
J
8

This is just a tiny bit better than your original suggestion:

local nullglob=$(shopt -p nullglob) ; shopt -s nullglob

... do whatever you want ...

$nullglob ; unset nullglob
Jemie answered 22/1, 2016 at 22:53 Comment(1)
I think you meant: $nullglob # unset nullglobCostanza
M
2

This may be close to what you want; as is, it requires executing a command to expand the glob.

$ ls
file1 file2
$ array=( $(shopt -s nullglob; ls foo*) )
$ ls foo*
ls: foo*: No such file or directory
$ echo ${array[*]}
file1 file2

Instead of setting array in the subshell, we create a subshell using $() whose output is captured by array.

Marna answered 3/2, 2012 at 14:2 Comment(1)
Doesn't work with spaces (no surprise): touch "file1" "file 2"; array=( $(shopt -s nullglob; ls foo*) ); set | grep ^array produces array=([0]="file1" [1]="file" [2]="2")Chayachayote
A
0

This is the simplest solution I've found:

For example, to expand the literal **/*.mp3 into a glob for only a particular variable, you can use

VAR=**/*.mp3(N)

Source: https://unix.stackexchange.com/a/204944/56160

Aiden answered 27/10, 2016 at 1:34 Comment(1)
Note that is a zsh feature not found in bash, which OP specified.Meda

© 2022 - 2024 — McMap. All rights reserved.