Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

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

share|improve this question
add comment

3 Answers

up vote 5 down vote accepted

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!' {} +
share|improve this answer
    
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
add comment

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
share|improve this answer
    
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
show 1 more comment

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!

share|improve this answer
    
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
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.