1

My shell is zsh. I tried these command on bash, which successed.

ignore_files=(
    local
    \*.local.\*
    .bundle
    Gemfile.lock
    )
ignore_command=""
for f in "${ignore_files[@]}"; do
    ignore_command="$ignore_command -x $f"
done
echo $ignore_command 
# print: -x local -x *.local.* -x .bundle -x Gemfile.lock 

sudo diff -ruNd  $ignore_command <path a> <path-b> 
# failed: diff: extra operand '<path-b>'


echo sudo diff -ruNd  $ignore_command <path a> <path-b> 
# sudo diff -ruNd  -x local -x *.local.* -x .bundle -x Gemfile.lock <path a> <path-b>

eval diff -ruNd  $ignore_command <path a> <path-b> # success

It seems that the variable isn't expanded in that command. I want to know why and how to make it like bash without eval.

1
  • 1
    The fact that it works in bash is purely accidental. As soon as you add a name with a space in your array, or a pattern that matches a name in the current directory, it will stop working in bash. Don't combine separate array items into a single string. You don't even need to do that in pure sh.
    – Kusalananda
    Dec 7, 2019 at 14:34

1 Answer 1

4

A command is a list of arguments, you want to use an array to store those arguments here:

ignore_files=(
    local
    '*.local.*'
    .bundle
    Gemfile.lock
)
ignore_command=()
for f in "${ignore_files[@]}"; do
    ignore_command+=(-x "$f")
done
print -r -- "${(q+)ignore_command[@]}"
sudo diff -ruNd  "${ignore_command[@]}" <path a> <path-b> 

You are probably being confused by the behaviour of Bourne-like shells where leaving a parameter expansion unquoted is a split+glob operator, so it turns a string/scalar variable into a list (or files matching patterns) upon expansion.

That misbehaviour was fixed by zsh and most other modern shells like rc, es, fish.

You could emulate that behaviour of the Bourne shells with set -o shwordsplit -o globsubst, but that would be going backward. Specifically, globsubst would be a problem in your case as *.local.* contains glob characters.

In zsh, if you want to split a variable or perform globbing upon its expansion, you have to ask it explicitly:

  • $=var, splits $var on $IFS characters.
  • ${(s:,:)var} splits on specific strings (here ,)
  • $~var: performs globbing (or pattern matching depending on context) upon expansion (for instance if $var contains *, $~var in list context will expands to the list of files in the current directory like the Bourne shell's $var)
  • $=~var: does both splitting and globbing so is like the $var of the Bourne shell.

In this particular case, you could also do:

sudo diff -ruNd  --exclude=$^ignore_files <path a> <path-b>

(where the ^ causes the array to expand like in fish or rc as --exclude=element1 --exclude=element2... It actually enables the rcexpandparam option for that one expansion like ~ enables globsubst and = shwordsplit).

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .