12

I want to lowercase every directories' name under a directory. With which commands can I do that?

erkangur
  • 235

4 Answers4

10

All the directories at one level, or recursively?

Zsh

At one level:

autoload zmv
zmv -o-i -Q 'root/(*)(/)' 'root/${1:l}'

Recursively:

zmv -o-i -Q 'root/(**/)(*)(/)' 'root/$1${2:l}'

Explanations: zmv renames files matching a pattern according to the given replacement text. -o-i passes the -i option to each mv command under the hood (see below). In the replacement text, $1, $2, etc, are the successive parenthesized groups in the pattern. ** means all (sub)*directories, recursively. The final (/) is not a parenthesized group but a glob qualifier meaning to match only directories. ${2:l} converts $2 to lowercase.

Portable

At one level:

for x in root/*/; do mv -i "$x" "$(printf %s "$x" | tr '[:upper:]' '[:lower:]')"; done

The final / restricts the matching to directories, and mv -i makes it ask for confirmation in case of a collision. Remove the -i to overwrite in case of a collision, and use yes n | for …. to not be prompted and not perform any renaming that would collide.

Recursively:

find root/* -depth -type d -exec sh -c '
    t=${0%/*}/$(printf %s "${0##*/}" | tr "[:upper:]" "[:lower:]");
    [ "$t" = "$0" ] || mv -i "$0" "$t"
' {} \;

The use of -depth ensures that deeply nested directories are processed before their ancestors. The name processing relies on there being a /; if you want to call operate in the current directory, use ./* (adapting the shell script to cope with . or * is left as an exercise for the reader).

Perl rename

Here I use the Perl rename script that Debian and Ubuntu ship as /usr/bin/prename (typically available as rename as well). At one level:

rename 's!/([^/]*/?)$!\L/$1!' root/*/

Recursively, with bash ≥4 or zsh:

shopt -s globstar  # only in bash
rename 's!/([^/]*/?)$!\L/$1!' root/**/*/

Recursively, portably:

find root -depth -type d -exec rename -n 's!/([^/]*/?)$!\L/$1!' {} +
  • At least on OS X this will fail if any directories are already lower case: mv -i a a give "mv: rename a to a/a: Invalid argument". – Janus Jan 05 '11 at 12:09
  • @Janus: Right, you get an error message, which is ugly (though harmless at the command line). But anyway I should have used zmv, which takes care of this case. – Gilles 'SO- stop being evil' Jan 05 '11 at 19:05
  • I then found about -execdir which is awesome: https://unix.stackexchange.com/questions/5412/lowercasing-all-directories-under-a-directory/494262#494262 I then found that it has some PATH madness and was sad :-( – Ciro Santilli OurBigBook.com Jan 13 '19 at 15:09
4

There isn't a single command that will do that, but you can do something like this:

for fd in */; do
  #get lower case version
  fd_lower=$(printf %s "$fd" | tr A-Z a-z)
  #if it wasn't already lowercase, move it.
  [ "$fd" != "$fd_lower" ] && mv "$fd" "$fd_lower"
done

If you need it to be robust, you should account for when there is already two directories that differ only in case.

As a one-liner:

for fd in */; do fd_lower=$(printf %s "$fd" | tr A-Z a-z) && [ "$fd" != "$fd_lower" ] && mv "$fd" "$fd_lower"; done
Shawn J. Goff
  • 46,081
  • This showed up midway through my writing the (basically the same) suggestion: for file in * ; do if [ -d "$file" ] ; then dest="$(echo $file | sed y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/)" ; [ "$dest" != "$file" ] && mv "$file" "$dest" ; fi ; done – frabjous Jan 05 '11 at 04:02
  • I was trying to work it out with find -type d, but couldn't quite get it – Michael Mrozek Jan 05 '11 at 04:07
  • 1
    My instinct would have been to do for fd in */;, thus avoiding the need for the check if it was a directory, but I have no idea if this instinct was a good one. – Steven D Jan 05 '11 at 04:27
  • Yep, for...*/ would be better. – Shawn J. Goff Jan 05 '11 at 04:42
  • 1
    @Shawn: tr already expects a range, so tr '[A-Z]' '[a-z]' translates [ to [ and ] to ] in passing. This is useless but harmlesss; however without the quotes the shell would expand the brackets if there was a file with a one-uppercase-letter name in the current directory. – Gilles 'SO- stop being evil' Jan 05 '11 at 08:21
0

find -execdir rename

This renames files and directories with a regular expression affecting only basenames.

So for a prefix you could do:

PATH=/usr/bin find . -depth -execdir rename 's/(.*)/\L$1/' '{}' \;

or to affect files only:

PATH=/usr/bin find . -type f -execdir rename 's/(.*)/\L$1/' '{}' \;

-execdir first cds into the directory before executing only on the basename.

I have explained it in more detail at: https://stackoverflow.com/questions/16541582/find-multiple-files-and-rename-them-in-linux/54163971#54163971

Ciro Santilli OurBigBook.com
  • 18,092
  • 4
  • 117
  • 102
0

I took this as a one-liner challenge :) First, establish a test case:

$ for d in foo Bar eVe; do mkdir -p dcthis/$d; touch dcthis/a${d}.txt; done
$ ls dcthis/
Bar     aBar.txt    aeVe.txt    afoo.txt    eVe     foo

I use find to spot the directories with uppercase letters and then downcase them via sh -c 'mv {} echo {} | tr [:upper:] [:lower:]'. Guess using sh -c is a bit hack-ish, but my head always explodes when I try escaping things for find directly.

$ (cd dcthis && find . -maxdepth 1 -type d -path '*[A-Z]*' -exec sh -c 'mv {} `echo {} | tr [:upper:] [:lower:]`' \;)
$ ls dcthis/
aBar.txt    aeVe.txt    afoo.txt    bar     eve     foo

Be warned: This solution does not check whether downcasing leads to collisions!

Janus
  • 1,533
  • checking for collisions is easy: mv -i. A bigger problem is that you haven't used proper quoting, so your command will fail if there are special characters (whitespace or \[*?) anywhere in the name. There's no point of using find unless recursing, and then you need find -depth, and -path must be -name. See my answer for working examples. – Gilles 'SO- stop being evil' Jan 05 '11 at 19:07
  • @Giles. Thanks! I saw your mv -i after writing this. Good point with the quoting... – Janus Jan 06 '11 at 05:45