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
+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.

  • 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 – Viziionary Mar 9 '15 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. – JakeGould Mar 9 '15 at 21:56
  • Oh ok, sorry about the lack of clarity. And thanks, cool! – Viziionary Mar 9 '15 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 '15 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. – JakeGould Mar 9 '15 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
    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 '15 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. – Viziionary Mar 9 '15 at 20:02
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' ); 
-1

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.

-2

There's more than one shell:

david_koontz@Macbook: more ~/bin/rename

#!/bin/csh -f

if  ( $1 != "" && $2 != ""  ) then

    foreach file ( `ls *$1*` )
    setenv target  `echo $file | sed s/"$1"/"$2"/`
        if ( -f $file ) then
            echo renaming $file to $target
            mv $file $target
        endif
    end
else
    echo "usage: $0 src_pattern_match  dest_pattern_substitution"
    exit -1
endif
exit 0

And for recursive renaming:

#!/bin/csh -f

    if  ( $1 != "" && $2 != ""  ) then

        # limit the depth search with -maxdepth:

        foreach file ( `find . -maxdepth 8 -iname \*$1\* -type f -exec echo \{\} \;`)
            setenv filehead $file:h
            setenv filetail $file:t
            setenv targettail `echo $filetail | sed s/"$1"/"$2"/`
            echo renaming $file $filehead/$targettail
            mv $file $filehead/$targettail
        end
    else
        "usage: $0 src_pattern_match  dest_pattern_substitution"
        exit -1;
    endif

exit 0;

For the recursive_rename you could potentially use an optional $3 to pass max depth or eliminate -maxdepth 8, which provides a recursive depth limit.

You could add an option -i could be added to mv or you could use and optional argument to enable interactive renaming.

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.