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?

codeforester
  • 39,467
  • 16
  • 112
  • 140
derobert
  • 49,731
  • 15
  • 94
  • 124

5 Answers5

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
estani
  • 24,254
  • 2
  • 93
  • 76
  • 1
    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. – derobert Feb 03 '12 at 17:07
  • 1
    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. – estani Feb 03 '12 at 18:27
  • 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. – derobert Feb 03 '12 at 19:04
  • 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 ;-) – estani Feb 06 '12 at 12:25
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.

Michael Kropat
  • 14,557
  • 12
  • 70
  • 91
  • 1
    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. – derobert Dec 14 '13 at 15:09
  • 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... – mivk Dec 23 '13 at 19:29
  • 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. – wheeler Sep 10 '19 at 14:30
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
codeforester
  • 39,467
  • 16
  • 112
  • 140
Martin Fick
  • 81
  • 1
  • 1
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.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • 6
    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")` – derobert Feb 03 '12 at 17:00
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

Community
  • 1
  • 1
laughing_man
  • 3,756
  • 1
  • 20
  • 20