12

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

9

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 5 '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 Jan 5 '11 at 19:05
  • I then found about -execdir which is awesome: unix.stackexchange.com/questions/5412/… I then found that it has some PATH madness and was sad :-( – Ciro Santilli 新疆改造中心996ICU六四事件 Jan 13 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
  • 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 5 '11 at 4:02
  • I was trying to work it out with find -type d, but couldn't quite get it – Michael Mrozek Jan 5 '11 at 4: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 5 '11 at 4:27
  • Yep, for...*/ would be better. – Shawn J. Goff Jan 5 '11 at 4: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 Jan 5 '11 at 8:21
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!

  • 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 Jan 5 '11 at 19:07
  • @Giles. Thanks! I saw your mv -i after writing this. Good point with the quoting... – Janus Jan 6 '11 at 5:45
0

find -execdir | rename

This would be the best way to do it if it weren't for the relative path madness, as it avoids Perl regex fu to only act on the basename:

PATH="$(echo "$PATH" | sed -E 's/(^|:)[^\/][^:]*//g')" \
  find a -depth -execdir rename 's/(.*)/\L$1/' '{}' \;

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

Unfortunately, I can't get rid of that PATH hacking part, find -execdir refuses to do anything if you have a relative path in PATH...: https://askubuntu.com/questions/621132/why-using-the-execdir-action-is-insecure-for-directory-which-is-in-the-path/1109378#1109378

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.