[ How to split on NULs in shell ]
I am using zsh as a shell.
I would like to execute the unix find command and put the result into a shell array variable, something like:
FILES=($(find . -name '*.bak'))
so that I can iterate over the values with something like
for F in "$FILES[@]"; do echo "<<$F>>"; done
However, my filenames contain spaces at least, and perhaps other funky characters, so the above doesn't work. What does work is:
IFS=$(echo -n -e "\0"); FILES=($(find . -name '*.bak' -print0)); unset IFS
but that's fugly. This is already a bit beyond my comfort limit with zsh syntax, so I'm hoping someone can point me to some basic feature that I never knew about but should.
Answer 1
I tend to use read
for that. A quick google search showed me zsh also seem to support that:
find . -name '*.bak' | while read file; do echo "<<$file>>"; done
That doesn't split with zero bytes, but it will make it work with file-names containing whitespace other than newlines. If the file-name appears at the very last of the command to be executed, you can use xargs
, working also with newlines in filenames:
find . -name '*.bak' -print0 | xargs -0 cp -t /tmp/dst
copies all files found into the directory /tmp/dst
. Downside of the xargs approach is that you don't have the filenames in a variable, of course. So this not always applicable.
Answer 2
Alternatively, since you're using zsh, you can use zsh's extended globbing syntax to find files rather than using the find command. As far as I know, all the functionality of find is present in the globbing syntax, and it handles filenames with whitespaces properly. See the zshexpn(1)
manpage for more info. If you use zsh on a fulltime basis, it's well worth learning the syntax.
Answer 3
The only way I figured out is using eval:
(zyx:~/tmp) % F="$(find . -maxdepth 1 -name '* *' -print0)"
(zyx:~/tmp) % echo $F | hexdump -C
00000000 2e 2f 20 10 30 00 2e 2f d0 96 d1 83 d1 80 d0 bd |./ .0../........|
00000010 d0 b0 d0 bb 20 c2 ab d0 a1 d0 b0 d0 bc d0 b8 d0 |.... ...........|
00000020 b7 d0 b4 d0 b0 d1 82 c2 bb 2e d0 9c d0 b8 d1 82 |................|
00000030 d1 8e d0 b3 d0 b8 d0 bd d0 b0 20 d0 9e d0 bb d1 |.......... .....|
00000040 8c d0 b3 d0 b0 2e 20 d0 93 d1 80 d0 b0 d0 bd d0 |...... .........|
00000050 b8 20 d0 be d1 82 d1 80 d0 b0 d0 b6 d0 b5 d0 bd |. ..............|
00000060 d0 b8 d0 b9 2e 68 74 6d 6c 00 2e 2f 0a 20 0a 6e |.....html../. .n|
00000070 00 2e 2f 20 7b 5b 5d 7d 20 28 29 21 26 7c 00 2e |../ {[]} ()!&|..|
00000080 2f 74 65 73 74 32 20 2e 00 2e 2f 74 65 73 74 33 |/test2 .../test3|
00000090 20 2e 00 2e 2f 74 74 74 0a 74 74 0a 0a 74 20 00 | .../ttt.tt..t .|
000000a0 2e 2f 74 65 73 74 20 2e 00 2e 2f 74 74 0a 74 74 |./test .../tt.tt|
000000b0 0a 74 20 00 2e 2f 74 74 5c 20 74 0a 74 74 74 00 |.t ../tt\ t.ttt.|
000000c0 2e 2f 0a 20 0a 0a 00 2e 2f 7b 5c 5b 5d 7d 20 28 |./. ..../{\[]} (|
000000d0 29 21 26 7c 00 0a |)!&|..|
000000d6
(zyx:~/tmp) % echo $F[1]
.
(zyx:~/tmp) % eval 'F=( ${(s.'$'\0''.)F} )'
(zyx:~/tmp) % echo $F[1]
./ 0
${(s.\0.)...} and ${(s.$'\0'.)...} do not work. You can use function:
function SplitAt0()
{
local -r VAR=$1
shift
local -a CMD
set -A CMD $@
local -r CMDRESULT="$($CMD)"
eval "$VAR="'( ${(s.'$'\0''.)CMDRESULT} )'
}
Usage: SplitAt0 varname command [arguments]
I should have used ${(ps.\0.)F}
, not ${(s.\0.)F}
:
% F=${(ps.\0.)"$(find . -maxdepth 1 -name '* *' -print0)"}
% echo $F[1]
./ 0
Answer 4
I've tried
F=(*)
and it handles even the files with newlines in the filename.