1

I have several directories within a directory I need renamed to include an underscore "_" character to the beginning of the name.

As a test, I created a parent directory whose contents have three directories:

dir1, dir2 and dir3; three files: file1, file2, file3

and three dot-prefix files:

.file1, .file2, .file3 

So far, I've tried using gfind, after having installed findutils on the Mac:

gfind . -type d -name '*' -printf "mv \"%h/%f\" \"%h/_%f\"\n" | sh

This returns:

mv: rename ./. to ./_.: Invalid argument

So, I tried using rename with find:

find . -type d -exec rename 's/^/_/' {} \;

This returns:

Can't rename '.' to '_.': Invalid argument
Can't rename './dir1' to '_./dir1': No such file or directory
Can't rename './dir2' to '_./dir2': No such file or directory
Can't rename './dir3' to '_./dir3': No such file or directory

Anyone happen to have a viable solution or know where I might be effing up?

4
  • 1
    your error msg indicated the issue as well.. find adds ./ to beginning.. so if your rename is perl based, try rename -n 's|^.*/\K|_|' and once you are okay with it, remove the -n option.. also, can use + instead of \; similar answer: unix.stackexchange.com/questions/310256/…
    – Sundeep
    Commented Sep 28, 2016 at 16:28
  • you may also be hitting the problem where you're renaming the parents before the children; look into find -depth
    – Jeff Schaller
    Commented Sep 28, 2016 at 17:15
  • Since you don't appear to need to recurse into subdirectories, can't you just use a */ glob e.g. for d in */; do echo mv -- "$d" "_$d"; done Commented Sep 28, 2016 at 17:56
  • Why don't you search the rename tag, there's plenty of similar questions with answers... Commented Sep 28, 2016 at 18:03

2 Answers 2

1

If I got it right, you have files and directories like this:

$ mkdir dir{1,2,3} ; touch file{1,2,3} .file{1,2,3}
$ ls -A
.file1  .file2  .file3  dir1  dir2  dir3  file1  file2  file3

and want to rename the dirs to _dir1, _dir2, _dir3?

That find/printf mv looks doable. Let's see what it prints:

$ find . -type d -name '*' -printf "mv \"%h/%f\" \"%h/_%f\"\n"
mv "./." "./_."
mv "./dir2" "./_dir2"
mv "./dir3" "./_dir3"
mv "./dir1" "./_dir1"

That first command will drop an error, since the "dot" is special and you can't rename it. But the others should be ok, and should rename the directories as you wanted:

$ find . -type d -name '*' -printf "mv \"%h/%f\" \"%h/_%f\"\n" | sh
mv: cannot move ‘./.’ to ‘./_.’: Device or resource busy
$ ls -d _dir*
_dir1  _dir2  _dir3

But piping commands to a shell is a bit ugly, and if the file names are strange enough, the results will be surprising (e.g. if they contain $ signs which will trigger variable or command substitution in the shell).

If the files are all on one level, then this should do:

for x in * ; do [ -d "$x" ] && mv "$x" "_$x" ; done

(Though if _$x exists already, $x will be moved into it.)

If you want to include directories whose names start with a dot, use shopt -s dotglob beforehand.

This is close, too:

find . -type d -exec rename 's/^/_/' {} \;

But since find gives rename the paths starting with ./, we need to take that into account. On one level only, this should do (changes the leading ./ to ./_):

find . -type d -exec rename 's,^./,./_,' {} \;

To get the directories on all levels, find -execdir may be easiest to use. It runs the command in the directory of the file. We need -depth to handle the renames in the correct order.

find . -depth -type d -execdir rename 's,^./,./_,' {} \;

Maybe add ! -name ., too. e.g.

$ mkdir foo foo/dir1 foo/dir2
$ find . -depth \! -name . -type d -execdir rename 's,^./,./_,' {} \;
$ ls  _foo
_dir1  _dir2
4
  • ikkachu> Truly, TRULY appreciate your help. I've been working on this for the better part of a week. After looking at this line: for x in * ; do [ -d "$x" ] && echo mv "$x" "_$x" ; done I believe I understand it more. You're assigning the action "x in *" to a variable. -d is for directories. Only if that succeeds "&&", echo mv that variable to include the underscore prefix. What I'm still not quite sure of is why echo needs to be used on mv.
    – lmind
    Commented Oct 5, 2016 at 3:05
  • @lmind, ah, the echo was basically just to view the commands before actually executing them, sometimes useful as a sanity check before automating e.g. file removal. Though here, the likely problem would be that the target exists already. And yes, the loop is repeated for all values that the asterisk resolves to. Could have used if ... ; then ... ; fi instead of the &&. Look here for a guide on shell loops and tests: mywiki.wooledge.org/BashGuide/TestsAndConditionals
    – ilkkachu
    Commented Oct 5, 2016 at 4:55
  • So, if I understand you correctly, echo isn't necessary, but useful for verbosity?
    – lmind
    Commented Oct 6, 2016 at 12:21
  • @lmind, with the echo the mv command will not be executed, just displayed. So in a case like this, it's purely a tool for checking correctness before actually doing anything.
    – ilkkachu
    Commented Oct 6, 2016 at 12:37
1

The problem with your first attempt is that you're attempting to rename . itself (the current directory), in addition to all the subdirectories. Skip it.

gfind . -mindepth 1 -type d -name '*' -printf "mv \"%h/%f\" \"%h/_%f\"\n" | sh

But actually, don't do this. never pipe stuff into sh. This doesn't work unless your file names happen not to contain any shell special characters. If you have a file called `rm -r ~` (a perfectly valid, if unusual, file name) then pop goes your home directory. Invoke a shell with -exec as I show below.

Your second attempt doesn't work because you're inserting the _ at the beginning of the full path, instead of adding it at the beginning of the base name, i.e. after the last /.

find . -mindepth 1 -type d -exec rename 's![^/]+$!_$&!' {} \;

Neither of these actually work if you have nested directories, because they rename the directories while find is traversing them. When you do this, you need to act on the contents of a directory before you act on the directory itself, with the -depth option.

find . -mindepth 1 -depth -type d -exec rename 's![^/]+$!_$&!' {} \;

More portably, call a shell and run mv. This works on any POSIX system.

find . -depth -type d -name . -o -exec sh -c '
    for d do mv "$d" "${d%/*}/_${d##*/}"; done
' sh {} +

If you there are no nested subdirectories, then you can use a simple shell loop. If you don't want to rename directories whose name start with a dot, just use the */ wildcard, which only matches directories and symbolic links to directories.

for d in */; do
  if [ "$d" = "*/* ] && [ ! -e "$d" ]; then continue; fi   # robustness in case there are no subdirectories
  if [ -L "$d" ]; then continue; fi     # optional: skip symbolic links to directories
  mv -- "$d" "_${d%/}"
done

If you want to rename all the directories including the ones whose name starts with a dot, then include the .* wildcard as well.

for d in .*/ */; do
  if [ "$d" = "*/* ] && [ ! -e "$d" ]; then continue; fi
  if [ -L "$d" ]; then continue; fi
  mv -- "$d" "_${d%/}"
done

Note that when you're moving foo to _foo, if there is already a directory called _foo, this moves the old foo to _foo/foo. To protect against this, add an explicit test.

Or, alternatively, use zsh. It's preinstalled on macOS. First run autoload -U zmv (put this in your .zshrc or in your script) to load the zmv function, then the nonrecursive version (including dot files, excluding symbolic links — (/D) is a set of glob qualifiers):

zmv -Q '*(/D)' '_$f'

or the recursive version (using modifiers to extract the dirname and the basename):

zmv -Q '**/*(/D)' '$f:h/_$f:t'

zmv will refuse to do a move if the target already exists.

You must log in to answer this question.

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