How ${*%word}
and the like work depends on the shell. POSIX leaves the result unspecified. There are two main plausible behaviors: the transformation (prefix or suffix removal) can be applied to each word, or to the result of joining the words. In shells that support arrays, it's natural apply the transformation to each word: that's what bash and ksh93 do. In shells that don't support arrays, it's natural join the words first (that's what ash/dash does). For example:
# No arrays: $* joined = 'abc abc'; strip off b* → 'a'
$ dash -c 'echo ${*%%b*}' _ abc abc
a
# Arrays: $* = ('abc' 'abc'); strip off b* from each element → ('a' 'a'); then join
$ bash -c 'echo ${*%%b*}' _ abc abc
a a
The first character IFS
is used to join the words that make up $*
. It only makes a difference to what is stripped if the pattern can match that character. For example:
# No arrays: $* joined = 'abc-def-ghi'; strip off -* → 'abc'
$ dash -c 'IFS=-; echo "${*%%-*}"' _ abc-def ghi
abc
# Arrays: $* = ('abc-def' 'ghi'); strip off -* from each element → ('abc' 'ghi'); then join
$ bash -c 'IFS=-; echo "${*%%-*}"' _ abc-def ghi
abc-ghi
When the substitution is in a word context, the expansion ends here. Word contexts include double quotes and assignments; see When is double-quoting necessary? and Expansion of a shell variable and effect of glob and split on it for more details. This explains echo "${*%.pdf}.pdf"
: the first character of IFS
is used for joining, and there is no subsequent splitting, hence the output in bash is a-b-c.pdf
. The value of both a
and b
is a-b-c.pdf
as well.
When the substitution is in a list context (i.e. unquoted), as in your first example, the result undergoes word splitting (and globbing). This is based on IFS
, hence a-b-c.pdf
is split into a
, b
and c.pdf
. The echo
command prints the three words with a space in between. Exactly the same thing happens with echo $a
and echo $b
in your example: the value of a
or b
is split at IFS
characters.
Zsh treats @
and *
differently. With *
as the parameter name, it applies the string-style behavior (join first then transform) inside double quotes, and the array-style behavior (transform each element) otherwise. On the other hand, the parameter @
is always treated as an array. Thus:
$ zsh -c 'echo "${*%.pdf}"' _ a.pdf b.pdf c.pdf
a.pdf b.pdf c
$ zsh -c 'echo ${*%.pdf}' _ a.pdf b.pdf c.pdf
a b c
$ zsh -c 'echo "${@%.pdf}"' _ a.pdf b.pdf c.pdf
a b c
$ zsh -c 'echo ${@%.pdf}' _ a.pdf b.pdf c.pdf
a b c
Unlike what happens in other shells, a string assignment does not cause $*
to be processed string-style: the double quotes are what matters. This explains why a=${*%.pdf}; echo $a
works like echo ${*%.pdf}
and not like a="${*%.pdf}"; echo $a
.
With IFS=-
, a dash is used when joining, which happens to *
whenever it's in a word context, whether due to double quotes or to a string assignment.
# ('a.pdf' 'b.pdf' 'c.pdf); strip each element → ('a' 'b' 'c'); print list
$ zsh -c 'IFS=-; echo ${*%.pdf}' _ a.pdf b.pdf c.pdf
a b c
# join → 'a.pdf-b.pdf-c.pdf'; strip the single word and print it
$ zsh -c 'IFS=-; echo "${*%.pdf}"' _ a.pdf b.pdf c.pdf
a.pdf-b.pdf-c
# ('a.pdf' 'b.pdf' 'c.pdf); strip each element → ('a' 'b' 'c'); `$*` in word context so join → 'a-b-c'; print word
$ zsh -c 'IFS=-; a=${*%.pdf}; echo "$a"' _ a.pdf b.pdf c.pdf
a-b-c
# join → 'a.pdf-b.pdf-c.pdf'; strip the single word; print the word
$ zsh -c 'IFS=-; a="${*%.pdf}"; echo "$a"' _ a.pdf b.pdf c.pdf
a.pdf-b.pdf-c
Note that you should almost never use $*
. It's only useful to join the positional arguments with IFS
, and it makes it impossible to distinguish IFS
characters created by the joining from IFS
characters that were already in the arguments. "$@"
is almost always the right form. Note that you do need the double quotes to avoid word expansions (even in zsh, although the effect of omitting the quotes is much smaller in zsh).
To make your script simple to understand, do one step at a time: strip off the suffix from each part, then join the parts. Use an array variable to store the intermediate result.
parts=("${@%.pdf}") # using @ because we want to have array behavior
IFS=-
joined="${parts[*]}" # using * and not @ for joining
echo "$joined.pdf"
This snippet works identically in bash and zsh.
${(j:-:)@%.pdf}.pdf
. The${(j:text:)array}
expansion joinsarray
with 'text'. So, in my example, it would join$@
with '-', apparently doing so after it first removes '.pdf' from the end of each element. – mangoduck Nov 19 '21 at 01:34