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 am trying to write a script that will replace spaces with "-" and make all letters lower case for all files in the current directory.

for x in 'ls'
    do
        if [ ! -f $x ]; then
            continue
        fi

        lc = `echo $x | tr '[A-Z]' '[a-z]'`
        if [ $lc != $x ]; then
            mv $x $lc
        fi
    done

find -name "* *" -type f | rename 's/ /-/g'

I get the following output: call: rename from to files...

However the names are not changing, example: 252680610243-Analyzed Sample2 2Jul12.txt

I changed the permissions with chmod 706, would this be causing the issue? What am I missing here?

Here is the output of bash -x lower.sh:

+ for x in ''\''ls'\'''
+ '[' '!' -f ls ']'
+ continue
+ find -name '* *' -type f
+ rename 's/ /-/g'
call: rename from to files...
share|improve this question
1  
Why don't you just comment out the mv and throw in an echo $lc or two so you can see what you are doing, then work from there? See also bash -x : tldp.org/LDP/Bash-Beginners-Guide/html/sect_02_03.html Learning to fish is better than asking for fish ;) –  goldilocks Mar 29 '13 at 13:46
    
You can use regular expressions for lowercasing characters without using tr. See stackoverflow.com/questions/689495/… –  Erik Mar 29 '13 at 13:48
    
I know the regex way to do this, I'm just trying to become a bit more familiar with bash scripting. See the OP for an update. What was interesting was that replacing mv... with echo $lc did not change the output of the code. –  Joe Mar 29 '13 at 13:59
1  
I'd think you want `ls` not 'ls' -- or better yet, just for x in *. My point about the echos and stuff was that you should consider the output of each command (eg. for x in 'ls' is not at all what you think it is). –  goldilocks Mar 29 '13 at 14:04
    
That was one bug, now this Unexpected arguments passed on cmd line ./lower.sh: line 8: [: !=: unary operator expected –  Joe Mar 29 '13 at 14:06

5 Answers 5

On Debian and derivatives (including Ubuntu), this is easy with a simple call to rename:

$ touch 'A B'
$ rename 'tr/ A-Z/-a-z/' -- *
$ ls
a-b

But your rename is a different utility with the same name.

share|improve this answer
    
I completely forgot about rename command :) –  Peperoncini Mar 29 '13 at 14:21
    
This didn't work for me –  Joe Mar 29 '13 at 16:29
    
You're probably using rename from util-linux and not the one shipped with perl then. –  Dennis Kaarsemaker Mar 29 '13 at 16:36
1  
Note that it would only translate ASCII letters, not other letters in the current locale if that locale is not ASCII based. –  Stéphane Chazelas Mar 29 '13 at 23:18
    
Hmm... I wonder if the perl rename accepts lc as argument. Perl's lc will do the non-ascii letters. –  Dennis Kaarsemaker Mar 29 '13 at 23:19

There are several problems with your script.

for x in 'ls'

You're iterating over a list containing one word: ls. You probably meant to write `ls` (with backticks), to parse the output of ls. Don't parse the output of ls: this won't work if the file names contain special characters such as spaces. The shell already has a built-in way of listing the files in a directory: wildcards. So write for x in *.

        if [ ! -f $x ]; then

This won't work if the file name contains whitespace and other special characters, because when you write $x outside quotes, the result is split into separate words (and each word is interpreted as a wildcard pattern, to boot). To avoid this effect, put $x in double quotes: if [ ! -f "$x" ]; then. You can remember complex rules, or just always use double quotes around variable substitutions.

Several other lines are broken due to the lack of double quotes. Just put them around every variable substitutions.

       lc = `echo $x | tr '[A-Z]' '[a-z]'`

This won't work due to the spaces around the equal sign: this line executes the lc command, passing it = as its first argument (and other arguments). An assignment in the shell must have no whitespace around the = sign.

You don't need brackets around the arguments of tr. Here the command happens to work because you're saying to replace [ by [ and ] by ] in addition to the lowercasing.

I recommend using dollar-parentheses $(…) rather than backticks `…` for command substitution. They're equivalent, except that backticks have complex rules when it comes to quoting things inside it, whereas $(…) has a very intuitive syntax. All in all, that command should be: lc=$(echo "$x" | tr A-Z a-z)

find -name "* *" -type f | rename 's/ /-/g'

Your error message shows that you have the rename command from the util-linux package and not the Perl script found on Debian and derivatives (including Ubuntu). The syntax you used only makes sense with the Perl script.

If you're going to use the Perl script, it can change the case to lowercase, so you don't need to do that beforehand. And you don't need to call find unless you want to rename files in subdirectories too (which your first part doesn't handle). If you want to rename all files in the current directory, you can just write:

prename 'tr/A-Z /a-z-/' -- *

If you only want to rename regular files (but not subdirectories, symbolic links, etc.), use find:

find . -type f -maxdepth 1 -exec prename 'tr/A-Z /a-z-/' {} +

If you want to act on files in subdirectories as well, and the directory names may contain spaces or uppercase letters, you have to take care to leave them alone. Since you're on Linux, you can use -execdir to call prename with an argument that doesn't contain a directory name.

find . -type f -execdir prename 'tr/A-Z /a-z-/' {} +

What if you don't have the Perl rename utility? The util-linux rename utility won't help you here. You can put one more replacement in your loop.

for x in ./*; do
  if [ -f "$x" ]; then
    y=$(echo "$x" | tr 'A-Z-' 'a-z ')
    mv "$x" "$y"
  fi
done

With bash, you don't need to call tr, you can use its string substitution constructs.

for x in ./*; do
  if [ -f "$x" ]; then
    lc=${x,,}            # convert all letters to lowercase
    y=${lc// /-}         # replace spaces by hyphens
    if [ "$x" != "$y" ]; then
      mv "$x" "$y"
    fi
  fi
done

If you want to act on files in subdirectories as well, you can use a recursive glob (enabled by the globstar option). Again, take care to leave the directory part alone if they may contain uppercase letters or spaces.

shopt -s globstar
for x in ./**/*; do
  if [ -f "$x" ]; then
    old_basename=${x##*/}
    new_basename=${old_basename,,}
    new_basename=${new_basename// /-}
    if [ "$new_basename" != "$old_basename" ]; then
      mv "${x%/*}/$old_basename" "${x%/*}/$new_basename"
    fi
  fi
done

In zsh, put autoload -U zmv in your .zshrc, and you can use the zmv with the glob qualifier . to match only regular files and parameter expansion constructs for the renaming:

zmv -Q '*(.)' '${(L)f// /-}'

To act on files in subdirectories as well:

zmv -Qw '**/*(.)' '$1${(L)2// /-}'
share|improve this answer
    
With find -exec prename, you'd need to only translate the basename, otherwise that won't work correctly if some dirnames have uppercase characters. Not all solutions behave the same for non-ASCII letters. –  Stéphane Chazelas Mar 29 '13 at 23:29
    
@StephaneChazelas True, I meant to add -maxdepth 1 as the original script in the question was recursive, but I forgot. The answer is long enough as it is, I'm sticking to ASCII letters like the question. –  Gilles Mar 29 '13 at 23:39
    
Also note that traditional SysV tr required the [...], so one may want to write it tr '[A-Z]' '[a-z]' to improve portability (as the extra [, ] don't harm for the BSD/POSIX-style tr). –  Stéphane Chazelas Mar 29 '13 at 23:50

The main problem is that you're not iterating over your files: you're iterating over a single string, "ls". You probably intended to use backticks instead of single quotes. Nevertheless, don't parse ls.

You also need to quote your variables especially since you know they contain whitespace.

Just in bash, you can do this:

declare -l lc    # whatever is stored in $lc is automatically lower cased
for x in *; do
    lc=${x//[[:space:]]/}  # replace all whitespace with nothing
    [ "$lc" != "$x" ] && mv -- "$x" "$lc"
done
share|improve this answer

You could be having problems with the "mv" command and spaces. I surrounded the original file with double quotes.

This script worked for me.

#!/bin/bash

for f in *
do
    g=$(echo $f | sed -e 's/\s/\-/g' | tr A-Z a-z)
    mv "$f" $g
done
share|improve this answer

In Bash 4 ${var,,} converts var to lowercase:

for f in *; do f2=${f,,}; mv "$f" "${f2// /-}"; done
share|improve this answer

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.