Use history modifiers.
for file in /tmp/text/foo.txt /foo.txt foo.txt mydir/; do
echo $file:h/bar/$file:t
done
Note that this gives a correct result except for files in the root directory on systems where //some/where
is different from /some/where
. In particular, it works if $file
doesn't have a directory part at all, whereas the naive version with ${file%/*}
doesn't. If $file
ends with a slash, bar
is inserted before the last nonempty path component, which may or may not be what you want.
With the ${VARIABLE/PATTERN/REPLACEMENT}
parameter expansion form, you can use the parameter expansion flag I
to select the nth match, but unfortunately you can't select from the end. So echo ${(I:3:)file/\///bar/}
works in this specific case, but it requires knowing exactly how many levels of directory they are. You can count them:
echo ${(I:${#file//[^\/]/}:)file/\///bar/}
But that's more complicated than splitting into parts, and it doesn't work any better if there's no slash in $file
.
With extended_glob
turned on, you can use the b
globbing flag in the pattern, and that lets you match the trailing non-slash characters (thanks to the #
glob operator) and use $match
to refer to the part matched by wildcards.
setopt extended_glob
for file in /tmp/text/foo.txt /foo.txt foo.txt mydir/; do
echo ${file/%(#b)([^\/]#)/bar/$match[1]}
done
The pattern is (#b)([^\/]#)
, matching the trailing non-slash characters and saving the matched part in $match[1]
. With zsh ≥5.9, you can write ${(*)file/%(#b)([^\/]#)/bar/$match[1]}
and it'll work even if extended_glob
is off. This is not simple, but it has the advantage of robustness: the result is correct even for the common case of a file name with no directory part and the edge case of a file in a root directory. It yields mydir/bar/
if $file
ends with a slash, which may or may not be what you want.
zmv '(**/)(*.txt)' '${1}bar/${2}'
. More details, including how to create destination directories withzmv
, in this answer. – Gairfowl Feb 19 '23 at 11:26