12

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

4 Answers 4

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!' {} +
3
  • 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". Commented Jan 5, 2011 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. Commented Jan 5, 2011 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 :-( Commented Jan 13, 2019 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
5
  • 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 Commented Jan 5, 2011 at 4:02
  • I was trying to work it out with find -type d, but couldn't quite get it Commented Jan 5, 2011 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. Commented Jan 5, 2011 at 4:27
  • Yep, for...*/ would be better. Commented Jan 5, 2011 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. Commented Jan 5, 2011 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!

2
  • 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. Commented Jan 5, 2011 at 19:07
  • @Giles. Thanks! I saw your mv -i after writing this. Good point with the quoting... Commented Jan 6, 2011 at 5:45
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

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.