0

I have tried a couple of approaches to find and rename some files that hold a backslash (mind you these files generated from a web app do not have the quoets) in a directory. I've tried with find and substitutions but they seem to fail at the parent level. It's basically like it rename the parent, but then fails to descend after to child folders because its referencing the older parent name when descending?

Folder Structure:

'ano\ther' -> 'chi\ld'

When I run:

for file in `find . -name '*\\\*'`; do mv -v "$file" "${file/\\/}"; done`

or

find . -exec rename 's/\\//g' {} +

or

find . -name "*\\\*" -exec rename 's/\\//g' {} +

They pretty much all say the same error, but do rename the parent or present dir, just fail on descending.

Can't rename ./ano\ter/yo\yo ./anoter/yoyo: No such file or directory

What would I be missing in my command?

AdminBee
  • 22,803

2 Answers2

4

You need to:

  • not loop over find's output like that
  • avoid the deprecated `...` form of command substitution
  • rename leaves before the branches they're on
  • only remove the \s in the name of the file, not its full path
  • avoid double quotes when wanting literal backslashes

So:

find . -depth -name '*\\*' -exec rename -d 's/\\//g' {} +

(here assuming a perl-based variant of rename and one that accepts that -d option to only rename the base name).

Or:

find . -depth -name '*\\*' -exec zsh -c '
  for file do
    mv -v -- $file $file:h/${file:t:gs/\\/}
  done' zsh {} +

Or in zsh, just:

autoload zmv
zmv -v '(**/)(*\\*)(#qD)' '$1$2:gs/\\/'

Here using the csh-style $var:s/string/replacement/ (with g to repeat), but you could also use the ksh-style ${var//pattern/replacement} (with // for the repeating form) if you preferred.

zmv does a depth-first traversal by default as that's what you generally want, the (#qD) is to also consider hidden files. zmv will also do some sanity checks and not rename anything if some problems are detected like if there's both a a\bc and a\b\c file which would both be renamed to abc.

  • As the major reason not to loop over find's output is that file names can contain (almost) any character, and this question is explicitly about weird characters in file names, I think that is very appropriate. – Henrik supports the community Mar 01 '24 at 16:46
  • Thank you for taking the time to provide several examples. I used find . -depth -name '*\\*' -exec rename -d 's/\\//g' {} + and it worked like a charm. So I guess my two problems I had. no -depth and adding triple backslash in '*\*' after -name was looking also at the path? as your comment of "only remove the \s in the name of the file, not its full path" – gstlouis Mar 01 '24 at 16:51
  • Inside backtick, there's an extra level of backslash handling even inside single quotes so you need 4 (or 3) backslashes for 2 to be passed to find, but using command substitution with split+glob is wrong as explained in the linked Q&A. -name looks at the name, but -exec cmd {} + passes the whole path and it's only the backslashes in the tail (as in the $file:t) you want to rename, not the head ($file:h). – Stéphane Chazelas Mar 01 '24 at 16:57
0

I guess that error message comes because you're trying to remove both 'es at once, and the directory './anoter' probably doesn't exist.

You can handle all the files in your directory structure by just adding -type f to your find. But then you'll still have the directories, if you know that you only have one level of subdirectories (or at least only 'es in the names in one level) you can just use -type d to find, but if you have e.g. ano\ther/d\ir/ the command will fail with the same error.

You might get something to work with using -execdir instead on -exec (and using -depth), but I've never tried -execdir so I'm just going on what a quick reading of the manpage suggests. Thinking about it, I'm not even sure you need to handle the files first if you do that.