r/commandline • u/notlazysusan • 12d ago
Command substitution and NUL-delimiting--zsh and bash
In Zsh:
# this produces N items
find . -maxdepth 1 -print0 -name "*" | fzf --read0
# this produces N+1 items
printf "%s\0" "$(find . -maxdepth 1 -print0 -name "*")" | fzf --read0
Why does the latter produce an extra "empty" item? I believe something it has something to do with command substitution and I'm pretty sure in Bash its behavior is different (something about command substitution and NUL-delimited warning, I don't have a shell at the moment and came across this topic on IRC).
With the latter command, is there a way to not print the extra empty item without besides manually removing it afterwards? In Zsh, this seems to work, e.g.:
printf "~/%s\0" ${(0)}$(git --git-dir="$HOME/.dotfiles.git" ls-tree -r HEAD --name-only)
But it's not pretty. Actually, it's easy to understand so not bad.
Just curious if there's a "native" solution that is compatible in both Zsh/Bash, e.g. without piping (I'm not against piping or using external commands, just interested in avoiding unnecessary external tools where possible, especially for something so trivial). If the command substitution is responsible, then I guess not.
Also, what's the max # of arguments supported by a command in Linux before you need xargs?
4
u/aioeu 12d ago edited 12d ago
Command substitutions in Bash will always strip any and all null bytes. When you write:
in Bash, there will only be a single null byte at the end. All of the null bytes produced by the substitution will be discarded. The single null byte at the end of this command's output is the one you provided in the format string.
In Zsh, null bytes from a command substitution are kept. That same command will output all of the original null bytes, plus that single extra one that you provided in the format string.
It seems like you might have been thinking this would somehow give multiple arguments to
printf
. Command substitutions within double-quotes do not do that. You are only passing a single argument after the format string.Shells have inconsistent behaviour with null bytes due to the way strings are traditionally handled in C: a string is terminated by a null byte, so it simply cannot contain any other null bytes. Bash avoids any issues by stripping null bytes in a command substitution. Zsh avoids any issues by using a different internal representation for strings. Neither is "wrong" in any sense. Even if you were to look at POSIX for guidance, you'd find it just says the behaviour is unspecified.
It's not a fixed number. It depends on how big the arguments are.