2

I am struggling with a command to rename directory and files.

  • OS: MACOS 11.4
  • Objective : rename directories and file by replacing the space with a underscore _
  • Command tried :
    find . -maxdepth 1 -iname "* *" -exec sh -c 'fichier="{}"; mv -- $fichier ${fichier// /_}' \;
    

I use find because I want to be able to navigate through the depth of the directory tree 1 level at a time.

The result of the command is:

usage: mv [-f | -i | -n] [-v] source target
       mv [-f | -i | -n] [-v] source ... directory

So instead of mv I used echo to check the command

find . -maxdepth 1 -iname "* *" -exec sh -c 'fichier="{}"; echo $fichier ${fichier// /_}' \;

That seems to be working as I expected:

./Job offers etc ./Job_offers_etc
./School results ./School_results
./Famille et Souvenirs ./Famille_et_Souvenirs
./KID education ./KID_education
./Template Open Office-MS Office ./Template_Open_Office-MS_Office
./Mode Emplois ./Mode_Emplois
./cartres de voeux ./cartres_de_voeux

What did I do wrong when used mv?

terdon
  • 242,166
Ajo
  • 83
  • 6
  • 1
    mv ./Job offers etc ./Job_offers_etc would be interpreted as having 4 paramters; you need to put "" around the first "Job offers etc" for it to be a single parameter. – Stephen Harris Jun 23 '21 at 11:43
  • Try this instead, and see if the output gives you a hint: find . -maxdepth 1 -iname "* *" -exec sh -c 'fichier="{}"; ls $fichier ${fichier// /_}' \;. That is, use ls instead of echo... ls should complain that the expanded filename, ${fichier// /_}, does not exist. But you might be surprised about the first... – C. M. Jun 23 '21 at 12:48
  • @ C.M. ls is complaining because as the time I execute the CLI the file actually does not and should not exist as the purpose is to create it. Or I miss understood your advise. – Ajo Jun 23 '21 at 14:08
  • Did you mean mv -- "$fichier" "${fichier// /_}"? Those quotes are important! – Toby Speight Jun 24 '21 at 10:34

3 Answers3

4

You are encountering one of the reasons why it is recommended to (almost) always quote your shell variables.

In your case, since you assign to the shell variable fichier a file name that contains whitespace, the shell will perform word splitting when you use that variable "as is", i.e. just as $fichier. That means that a directory Job offers etc as argument to mv would be interpreted as trying to move the three files Job, offers and etc to the destination directory Job_offers_etc(1).

In order to avoid the problem, place double-quotes around the variable reference, as in "$fichier".


(1) This can actually be dangerous; in some settings, if the destination directory doesn't exist, the command may lead to three files being renamed to the same name, effectively overwriting the first two by the third one.

AdminBee
  • 22,803
  • Alternatively, use zsh (the default macos shell) instead of sh which has much saner quoting rules by default. – cobbal Jun 23 '21 at 19:48
  • @ Cobbal noted and good comment. though I like my tools to work on different OS. – Ajo Jun 24 '21 at 07:06
3

If you have the perl rename utility, File::Rename, already installed, this is easy. If you don't already have it, you should install it - it makes difficult bulk-renaming operations trivially easy.

If you have Homebrew installed, you can install it with brew install rename (see rename on brew), otherwise you can install it with cpan. Again, if you don't have Homebrew installed on your Mac, you should, it gives you access to thousands of packages.

Then you can replace spaces with underscores in all filenames + directory names in the current directory with just:

rename -n 's/ /_/g' *

The -n option makes this a dry-run, it won't actually rename any files, it will just show you what would be renamed. Once you're sure it does what you want, remove the -n option or replace it with -v for verbose output.

There's little or no need to exclude files that don't contain a space because perl rename will only attempt to rename a file if the filename was actually changed by your rename script, and the script above won't change filenames without a space.

It will also refuse to rename a file over an existing filename unless you force it to with -f.

You can use it with find if you want, and it understands NUL-separated input (so it works with any filenames, even those with linefeeds in them). e.g.

find . -maxdepth 1 -iname "* *" -print0 | rename -0 -n 's/ /_/g'
cas
  • 78,579
  • good option. I use macport but not yet brew. I will give it a try or find rename in macport. Still does any body have a answer to the question with find. I hate not to understand why I do wrong. :) – Ajo Jun 23 '21 at 14:06
  • The problem was that you weren't quoting your variables when you used them. i.e. your script should have had "$fichier" rather than just $fichier. Without the quotes, the shell will word-split the contents of the variables on whitespace characters (spaces, tabs, newlines, etc). e.g. the first filename will be split into three: ./Job, offers, and etc – cas Jun 23 '21 at 14:55
  • Also, your find ... -exec command would have been better written as something like find . -maxdepth 1 -iname "* *" -exec sh -c 'for fichier in "$@"; do mv -- "$fichier" "${fichier// /_}"; done' find-sh {} +. As well as actually working because the variable is being double-quoted, that runs sh as few times as possible, with as many files as possible as args, instead of forking sh once for each filename. So, it's much faster. – cas Jun 23 '21 at 15:00
  • thanks. I really feel I learning good stuff here. – Ajo Jun 23 '21 at 18:25
2

Because of the combination of find and sh -c and the need to quote variables, it might be easier to use a for loop.

for file in *\ *
do
  newfile=$(echo "$file" | tr ' ' _)
  mv -i "$file" "$newfile"
done
  • thanks. your idea actually works for the first level in the tree. I want to have a new CLI to be able to access other levels of the tree. so I try with find. – Ajo Jun 23 '21 at 14:00