I have a script that searches for space characters ' ', exclamation marks '!' and dollar signs '$' in filenames and replaces each with an underscore '_'. However, it does not handle file names with exclamation marks in them, at all. Files to be renamed are searched for in the current folder and all sub-folders up to a certain depth (not very relevant here). Folder names should also be changed in the same way, that's why the depth is staggered.

This is the desired renaming scheme:

This is cool!.txt     -->    This_is_cool_.txt
Thank$.log            -->    Thank_.log
Foo 1/Bar 2/a.txt     -->    Foo_1/Bar_2/a.txt

Here is the script:

#!/bin/tcsh -f
foreach n ( 1 2 3 4 5 6 7 8 )
    find . -mindepth $n -maxdepth $n -name '*[ $\!]*' | fgrep -v \" | \
    awk '{printf "mv -i -- \"%s\" \"%s\"\n", gensub("!","\\\\\!","g",$0), gensub(" ","_","g",gensub("\\$","\\\\$","g",gensub("!","\\\\\!","g",$0)))}' | tcsh -cf

I have varied the number of backslashes '\' before both exclamation marks but to no effect. Error messages may look like this (even number of backslashes):

awk: cmd. line:1: warning: escape sequence `\!' treated as plain `!'

or the shell script (apparently) opens an interactive shell?! Sometimes nothing happens at all (I suppose when there are too many backslashes in the first argument to mv and the file is not found).

PS: As you can see I excluded files with a double quote '"' in them because I also could not get those to work, either. If you could suggest how to handle those as well, all the better!

  • Did you try with exactly 4 backslashes? Also, are you open to other shells? [t]csh is a notoriously bad tool for scripting.
    – terdon
    Commented Jun 9, 2021 at 19:25
  • @terdon Thanks, 4 is one of those even cases with awk: cmd. line:1: warning: escape sequence '\!' treated as plain '!'. I did try | bash but that did not work either somehow. If you have an answer with a different shell I am very open to it, though!
    – Ned64
    Commented Jun 9, 2021 at 20:59
  • Why do you have the loop from 1 to 8 for -mindepth and -maxdepth? Is it only to avoid errors caused by renaming directories before possibly renaming files in them? Or do you want to exclude depth >8?
    – Bodo
    Commented Jun 9, 2021 at 21:19
  • Don't write [t]csh scripts - google "csh why not".
    – Ed Morton
    Commented Jun 9, 2021 at 21:45
  • 1
    @EdMorton Point taken. I prefer tcsh in interactive mode. Perhaps I need to switch for scripts.
    – Ned64
    Commented Jun 9, 2021 at 22:42

2 Answers 2


It would be much easier just to call the mv directly rather than synthesising the command in awk and piping it to a shell

find -mindepth 1 -maxdepth 8 -type f -name '*[ !$]*' -execdir
    bash -c 'for f in "$@"; do echo mv -- "$f" "${f//[ !$]/_}"; done' _ {} +

This version renames files but not directories (as stated in the question). If you actually meant to rename directories too, use this variant,

find -mindepth 1 -maxdepth 8 -depth -name '*[ !$]*' -execdir
    bash -c 'for f in "$@"; do echo mv -- "$f" "${f//[ !$]/_}"; done' _ {} +

In both cases remove echo when you're happy the command is going to do what you expect. Use mv -i for interactive renames. The underscore in the bash -c '...' _ {} becomes the "name" of the code running in the subshell. It's mapped to $0 and will be used for reporting code errors (if any), so you could put bash there or even subshell - it's just a label.

  • Thanks, I just added the detail that folder names may contain spaces etc. which should also be replaced. That's why I counted up the the depths separately in find.
    – Ned64
    Commented Jun 9, 2021 at 21:53
  • Please explain the underscore _ before the braces {} - is that some bash or find magic not found in the respective manuals?
    – Ned64
    Commented Jun 9, 2021 at 22:13
  • I never want to overwrite files non-interactively and do not want to restrict file names (not to start with a dash/minus) so I use mv -i -- - I suppose this could be changed here?
    – Ned64
    Commented Jun 9, 2021 at 22:39
  • OK, thanks for the explanation! The script works like a charm :-)
    – Ned64
    Commented Jun 10, 2021 at 10:23

This script assumes you don't want to limit the depth for find and that you want to replace all characters $!"' with _.

As the question doesn't contain information about the operating systems or the specific version of the tools, I try rely on POSIX specification only and not to require specific versions like GNU.

find . -depth -name '*[ $!"]*' | while IFS= read -r file
   newname="$(echo "${file##*/}" | tr ' $!"' '____')"
   echo mv "$file" "$dir/$newname"

The script only slightly tested. If it prints the correct mv commands remove the echo.


find . -depth... makes sure the files are printed before the directory if both contain characters to be replaced. This way we will rename the files first, then the parent directory.

The parent directory name is separated with dir="${file%/*}"` and left unchanged.

The file name (or last directory name) is extracted and changed with newname="$(echo "${file##*/}" | tr ' $!"' '____')".

Further improvements

To handle other "strange" characters in file names it is be better to call the code in the loop body directly from find's -exec action like in roaima's answer.

find . -depth -name '*[ $!"]*' -exec sh -c 'for file in "$@" ; do dir="${file%/*}" ; newname="$(echo "${file##*/}" | tr '\'' $!"'\'' "____")" ; echo mv "$file" "$dir/$newname"; done' _ {} +

See also https://mywiki.wooledge.org/BashFAQ/020

Using -execdir as in the other answer would avoid splitting file name and parent directory, but this action is not defined by POSIX.

It is possible to replace the echo... |tr... combination with a replacement by the shell using something like "${newname//[ $!\"]/_}".


You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .