2

The script given below places "underscore" instead of "whitespace" in all file names that are in a certain folder. I'm having trouble making a shell script that places "underscore" instead of "whitespace" in the names of all subfolders and files contained in them and not just in a folder.

Does anyone have any tips on how can I do this?

Here is my code:

#!/bin/bash
ls | while read -r FILE; do
  mv -v "$FILE" `echo $FILE | tr ' ' '_'`
done
3

Use find to search in dirs and subdirs:

while IFS='' read -r -d '' fname ; do
   nname="${fname##*/}"
   mv -v -n "${fname}"  "${fname%/*}/${nname//[[:space:]]/_}"
done < <(find "$(pwd)"  -name "* *" -type f  -print0)

find "$(pwd)" -type f -print0 - Prints all the found file paths in current dir and subdirs.

With the process substitution output of find command is sent to while loop where it reads variable fname.

nname="${fname##*/}" - Extracts file name from path

"${fname%/*}" - extracts the path

"${nname//[[:space:]]/"_"}" - replaces spaces in the filename with _

"${fname%/*}/${nname//[[:space:]]/"_"}" - path/new_filename

  • You need to remove the double-quotes around the underscore; since it's already in double-quotes, they apparently get treated as literal characters and end up in the filename. Also, I recommend adding double-quotes around $(pwd) in case it contains spaces (or just use . instead), and add -name "* *" to the find command to skip files that don't have spaces, and add either -i or -n to the mv command to avoid data loss if there's a filename conflict. – Gordon Davisson May 16 '18 at 1:13
  • Good points. Double quotes inside double quotes work in my shell but I will change that too. Thanks. – user780735 May 16 '18 at 1:27
  • 1
    Interesting. Apparently the treatment of double-quotes in that context changed sometime between bash v4.2 and bash v4.4. I was testing with bash v3; you probably have a newer version. – Gordon Davisson May 16 '18 at 1:56
3

Use rename utility instead, e.g.

rename "s/ /_/g" *

Perl-powered file rename script with many helpful built-ins.


For recursive renaming, try:

rename "s/ /_/g" **/*.*

where ** is a Bash globbing option (enable by shopt -s globstar).

Alternatively use find, e.g.

find . -type f -execdir rename "s/ /_/g" {} ';'
0

In bash (not sure on derivatives and sh!) you can use variable substitution etc in variables

FILE="file name"
touch "$FILE"
mv -v "$FILE" "${FILE// /_/}"

HOWEVER THIS atleast does not deal with other space characters (tab etc)

  • Thank you Wilf! I still can not. Soon I will see your suggestion more carefully. The script I put above inserts "underline" between all words that have "whitespace". I would like the same thing to be done in the name of the files that are in subfolders – Rafael May 15 '18 at 21:41
0

My approach:

find . -depth -name "* *" -execdir bash -c 'pwd; for f in "$@"; do mv -nv "$f" "${f// /_}"; done' dummy {} +

Multi-line version for readability:

find . -depth -name "* *" -execdir \
   bash -c '
      pwd
      for f in "$@"; do
          mv -nv "$f" "${f// /_}"
      done
   ' dummy {} +

Explanation:

  • find . -name "* *" finds objects that need to be renamed. Note find is very flexible with its tests, so if you want (e.g.) to rename directories only, start with find . -depth -type d -name "* *".
  • -execdir executes the given process (bash) in a directory where the object is, so any path passed by {} is always like ./bar, not ./foo/bar. This means we don't need to care about the whole path. The downside is mv -v won't show you the path, so I added pwd just for information (you can omit it if you want).
  • bash lets us use the "${baz// /_}" syntax.
  • -depth ensures the following won't happen: find renames a directory (if applicable) and then tries to process its content by its old path.
  • {} + is able to feed bash with multiple objects (contrary to {} \; syntax). We iterate over them with for f in "$@". The point is not to run a separate bash process for every object since creating a new process is costly. I think we cannot easily avoid running separate mv-s; still, reducing the number of bash invocations seems a good optimization (pwd is a builtin in bash and doesn't cost us a process). However -execdir ... {} + won't pass files from different directories together. By using -exec ... {} + instead of -execdir ... {} + we may further reduce the number of processes but then we need to care about the paths, not just filenames (compare this other answer, it seems to do a decent job but while read slows it down). This is a matter of speed versus (relative) simplicity. My solution with -exec is down below.
  • dummy just before {} becomes $0 inside our bash. We need this dummy argument because "$@" is equivalent to "$1" "$2" ... (not "$0" "$1" ...). This way everything passed by {} is available later as "$@".

More complex, slightly optimized version (various ${...} tricks taken from another answer):

find . -depth -name "* *" -exec \
   bash -c '
      for f in "$@"; do
          n="${f##*/}"
          mv -nv "$f" "${f%/*}/${n// /_}"
      done
   ' dummy {} +

Another (experimental!) approach involves vidir. The trick is vidir uses $EDITOR which may not be an interactive editor:

find . -name "* *" | EDITOR='sed -i s/\d32/_/g' vidir -

Caveats:

  • This will fail for file/directory names with special characters (e.g. newlines).
  • We can't use s/ /_/g directly, \d32 is a workaround.
  • Because of how vidir works, the approach would get tricky if you would like to replace a digit or a tab.
  • Here vidir works with paths, not only filenames (base names), thus renaming files only (i.e. not directories) may be hard.

Nevertheless if you know what you're doing then this may do the job even faster. I don't recommend such (ab)use of vidir in general case though. I included it in my answer because I found this experimental approach interesting.

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.