2

I have been naming files, of out habit as a programmer working on a server command line, with underscores “_” rather than spaces “"—because from the command line you have to escape spaces—but that is unnecessary outside of the command line and is quickly getting old. I would like to keep everything uniform while switching to spaces.

From the “Terminal,” I’m wondering how can I convert all “x”s to “y”s in filenames for a specific directory on Mac OS X? Note that “x” means just anything and with “y” simply being a space.

I’d like to do this recursively, handling all sub-directories rather than only one folder at a time.

2
  • 1
    With y being a space? May want to clarify that. Otherwise you will get answers that break with spaces in file names, as ever so often.
    – slhck
    Feb 11, 2015 at 17:02
  • just reverse this whitespace-to-underscore solution
    – meatspace
    Feb 11, 2015 at 17:04

5 Answers 5

2
+25

From the “Terminal,” I’m wondering how can I convert all “x”s to “y”s in filenames for a specific directory on Mac OS X? Note that “x” means just anything and with “y” simply being a space.

I’d like to do this recursively, handling all sub-directories rather than only one folder at a time.

Adapting the some the basic sed concept that @meatspace refers to in his comment (which is located on this page) and adding find into the mix I came up with this script:

find . -type d -path '*/\.*' -prune -o -not -name '.*' -type f |\
  while read FULL_ITEM_PATH
  do
    FILE_DIRNAME=$(dirname "${FULL_ITEM_PATH}");
    FILE_BASENAME=$(basename "${FULL_ITEM_PATH}");
    mv "${FILE_DIRNAME}"/"${FILE_BASENAME}" "${FILE_DIRNAME}"/"$(echo $FILE_BASENAME | sed -e 's/_/ /g')";
  done

That will use find to find all files from the current directory you are in—including child directories/files—and then search for underscores in filenames (_) and change them to spaces () as desired. The script ignores “dot files”/“invisible files” like .DS_Store via -prune -o -not -name '.*' and then the core logic only acts on the actual filename—not the directory name—by separating the file basename from the directory name.

I created a test directory tree on my Mac OS X 10.9.5 (Mavericks) machine with files that contain underscores (_) in their names—with some nested in a child directory—like this:

./foo_bar
./foo_bar_two
./foo_bar_two_three
./foo_bar_two_three_bleagh.txt
./nested/foo_bar
./nested/foo_bar_two
./nested/foo_bar_two_three
./nested/foo_bar_two_three_bleagh.txt
./nested_foo/foo_bar
./nested_foo/foo_bar_two
./nested_foo/foo_bar_two_three
./nested_foo/foo_bar_two_three_bleagh.txt

Then I ran that script and they were all automatically changed to use spaces () as desired like this:

./foo bar
./foo bar two
./foo bar two three
./foo bar two three bleagh.txt
./nested/foo bar
./nested/foo bar two
./nested/foo bar two three
./nested/foo bar two three bleagh.txt
./nested_foo/foo bar
./nested_foo/foo bar two
./nested_foo/foo bar two three
./nested_foo/foo bar two three bleagh.txt

While the script works, if you want to do a simple “dry run” to see what files the script would act on replace the mv command to echo like this:

find . -type d -path '*/\.*' -prune -o -not -name '.*' -type f |\
  while read FULL_ITEM_PATH
  do
    FILE_DIRNAME=$(dirname "${FULL_ITEM_PATH}");
    FILE_BASENAME=$(basename "${FULL_ITEM_PATH}");
    echo "${FILE_DIRNAME}"/"${FILE_BASENAME}" "${FILE_DIRNAME}"/"$(echo $FILE_BASENAME | sed -e 's/_/ /g')";
  done

The nice thing about using echo to debug is you can also see exactly what is happening in the core of your script. So while I have coded this script to fit the exact needs as specified in this question, feel free to experiment with it to adapt it to newer needs/ideas if they come up.

5
  • 1
    For me, this returns errors for everything: mv: rename ./Springhill/WPCS_Head_Start_Pre-School/WPCS_Head_Start_Pre-School_Office_Systems/.DS_Store to ./Springhill/WPCS Head Start Pre-School/WPCS Head Start Pre-School Office Systems/.DS Store: No such file or directory
    – J.Todd
    Mar 9, 2015 at 21:29
  • @MediaWebDev You mentioned only the files used “_” in their names, not the directories. So I have upgraded the simple script to be a little bit more robust. It now separates the directory path from the filename, runs the sed logic only on the filename and then uses mv to move/rename the files without acting on the directories. I also added logic to ignore invisible files like .DS_Store; basically any file that begins with . in the name. Mar 9, 2015 at 21:56
  • Oh ok, sorry about the lack of clarity. And thanks, cool!
    – J.Todd
    Mar 9, 2015 at 21:58
  • I'd recommend adding -depth to the find so that you don't change higher level directory (folder) names before changing the files within them.
    – roaima
    Mar 9, 2015 at 22:41
  • @roaima The question and this script is designed to only act on file names, not directories. So that is not really a concern. Directories will never be affected by this script. Mar 9, 2015 at 22:48
0

When in relevant directory:

for f in in *; do echo mv "$f" "${f/_*_/ }"; done

This will list the changes to be made - remove echo when you want it to actually do the rename. Like everything *nix, there are a million ways to do this, but the above is pretty clear. If you do this task often, consider saving it in bin as a script.

2
  • 2
    The replacement expression shown will remove any substring between underscores, so foo_bar_baz.txt would be renamed to foo baz.txt, for example. Consider ${f//_/ } to replace all underscores with spaces without otherwise modifying the filename.
    – zackse
    Feb 17, 2015 at 3:57
  • 1
    I meant for it to change all file names in the directory, as in, the files in sub-directories too. Every file-name in the directory, and it's contents, recursively.
    – J.Todd
    Mar 9, 2015 at 20:02
0

How about this one liner? (I hope pasting my command here does not screw up the spaces)

for f in `find . -type f -name '*_*'` ; do echo mv "$f" "${f//_/ }"; done        

Seems to work when I tried it. (You need to remove that echo when ready)

Doesn't do any directory renaming. I think that's what you wanted? Recursion seems to work fine. Also handles multiple underscores correctly.

0

For batch/ complex renaming tasks use rename command

For example to replace space with _ in all the .txt files in current folder

rename 's/ /_'  *.txt

To rename all files recursively

find /path/to/target -type f -exec rename 's/ /_/' '{}' \;
0

I'd probably use Perl to do it.

#!/usr/bin/perl

use strict;
use warnings;

use File::Find;

sub rename_files {
   #skip directories
   return if -d $File::Find::name;

   my $newname = $File::Find::name;
   #turn underscores into space. 
      $newname =~ s/_/ /g; 
   rename $File::Find::name, $newname;
}

find ( \&rename_files, 'path_to_change' ); 

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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