5
file=/tmp/text/foo.txt

Now I want to add a subfolder /bar in this directory structure using parameter expansion syntax. This line works:

echo "${file%/*}/bar/${file##*/}"

Now I'm wondering if I can make it even shorter with ${parameter/%pat/string} syntax. I'd like to make / as the pat, and replace it with /bar/. But I don't know how to tell the system which / is the pat part. I tried to escape it with \, but it didn't work. The code I wrote is:

echo "${f/%///bar/}"

or

echo "${f/%\///bar/}"

But none of them worked.

preachers
  • 151
  • If you're going to use this to move files, look at zmv: zmv '(**/)(*.txt)' '${1}bar/${2}'. More details, including how to create destination directories with zmv, in this answer. – Gairfowl Feb 19 '23 at 11:26

1 Answers1

5

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.