3

I'm looking to write a script that will find and replace any characters other than [^A-Za-z0-9._-] in a specific file type (in this case .avi) with an underscore _. I want this to exec recursively from the current path. I'm using Ubuntu.

I do not want or need to change any folder names as I have control over those at the time of creation. Here's what I have now after referencing the following link:

SE reference

find . -depth -exec rename 's!([^/]*\Z)/[^A-Za-z0-9._-]/_/g' *.avi {} +

I'm clearly missing something. Please help. :)

  • I think you need \; at the end of the -exec in find. Not sure what the '+' does. – Elliptical view Jan 21 at 5:54
  • Tip: First get the -exec to work with something simple like -exec ls {} \; Also get the rename to work separately outside of the find. – Elliptical view Jan 21 at 5:56
  • 1
    @Ellipticalview The {} + will be replace with a batch of pathnames, this is standard find syntax. – Kusalananda Jan 21 at 6:53
3

Why your code doesn't work

The wildcard pattern *.avi is expanded by the shell that runs find before running find, so its effect depends on whether there are *.avi files in the current directory or not. See find not recursive when file at top for more explanations. To expand *.avi in subdirectories, you'd need to do three things differently: quote the pattern so that the original shell doesn't expand it; arrange to run an additional shell in each subdirectory to perform the wildcard expansion; and look for directories only with the find command rather than any file type.

In addition, your code ends up calling rename on every file at any level under the current directory, including on subdirectories themselves, via {} +. So rename operates on directories, not just regular files.

Furthermore there's a syntax error in your Perl code.

Working solution with zsh

autoload -Uz zmv # best in ~/.zshrc

zmv -n '(**/)(*.avi)(#qD^/)' '$1${2//[^a-zA-Z0-9._-]/_}'

^/ is to select any type of file other than directory. Replace with . for regular files only. -n is for dry-run. Remove when happy.

Working solution with find and rename

With the perl-based variants of rename and a find implementation that supports -execdir:

LC_ALL=C find . -depth -name '*[!a-zA-Z0-9._-]*.avi' ! -type d -execdir \
  rename 's/[^a-zA-Z0-9._-]/_/g' {} +

There are a few caveats with that approach though:

  • That runs at least one rename instance per directory containing files to rename (one rename per file with some find implementations/versions where -execdir ... {} + is actually the same as -execdir ... {} \;. (zmv runs one mv per file, but you can make mv builtin with zmodload zsh/files to speed it up).
  • With -execdir, find runs the command in the directory that contains those files and passes a path relative to that directory to the command. Some find implementations (the GNU one) add a ./ prefix to the files, some don't. Some variants of rename do accept options after the perl expression, which means that if you have a file whose name starts with -, it could cause problem.
  • we have to use LC_ALL=C for -name to work even if file names contain sequences of bytes that otherwise wouldn't form valid characters in the locale. rename inherits that and anyway in most variants only works with ASCII. That means however that it will replace multi-byte characters with as many _ as the character has bytes. For instance, it would rename a UTF-8 stéphane to st__phane instead of st_phane. zsh is OK because it will convert both multi-byte characters and all bytes that can't be decoded to characters into one _ character each.
  • contrary to zsh's zmv, it won't perform sanity checks (like that 2 files are not going to end up having the same name like a+b.avi and a@b.avi) prior to start renaming. rename should however not overwrite existing files.
| improve this answer | |
  • Thank you for the insight and examples. Now, specifically referring to zsh's zmv, what is the behavior when an open file is encountered? – senor_nose Jan 26 at 20:29
  • @senor_nose, zmv eventually calls mv. Whether the file is open or not has no influence. Renaming files is just changing the contents of directory files. – Stéphane Chazelas Jan 26 at 20:31
  • Thank you again for following up.If a file was being written to, and the file name is changed, then there would be the possibility of a write severance (i.e., application continues to write to a file that has the character I'd like to remove, it gets removed by mv, application has lost it's write destination). Is there a way to manage this? – senor_nose Jan 27 at 2:23
  • @senor_nose, that kind of problem could only arise when mv moves the file to a different file system (where mv needs to do a copy+delete). If it only does a rename, the file renames the same file all along, only the directory entries that refer to the file are renamed, again, mv (the rename() system call) modifies the directory, not the file (it only changes the ctime (change status time) of the file). – Stéphane Chazelas Jan 27 at 7:16
2

Assuming that

rename 's/[^A-Za-z0-9.-]/_/g' -- *.avi

would properly rename all the files with a .avi filename suffix in the current directory, you would be able to apply this to all directories beneath the current one (including the current one) with

find . -type d -exec sh -c '
    for dirpath do
        ( cd "$dirpath" && rename "s/[^A-Za-z0-9.-]/_/g" -- *.avi )
    done' sh {} +

For batches of found directories, this would execute a short in-line script that changes into the found directory and executes the rename command.

| improve this answer | |
  • 1
    @PauloTomé That sh will be placed in $0 and used in any error messages that the shell produces. It is not part of the positional parameters. – Kusalananda Jan 22 at 15:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.