8

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...
Joe
  • 351
  • 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 : http://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 http://stackoverflow.com/questions/689495/upper-to-lower-case-using-sed – Nova 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
  • Evidently $x is empty then. This is why the right hand (or both) side(s) in bash comparisons are often quoted ("$x" expands to an empty string if there is nothing in $x, but just plain $x expands to nothing if there is nothing in $x, so what bash sees is if [ $x != ]. Also, in general use double [[ instead of [ with bash. – goldilocks Mar 29 '13 at 14:11
  • Sorry, that should have read, "what bash sees is if [ $lc = ]". – goldilocks Mar 29 '13 at 14:20
  • @Erik, so what - why is regex and sed better than tr and a range? I prefer the tr, and would bet it was faster – Graham Nicholls Aug 05 '18 at 15:29

5 Answers5

10

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.

6

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// /-}'
  • 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 'SO- stop being evil' 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
3

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
glenn jackman
  • 85,964
2

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
T.P.
  • 203
2

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

for f in *; do f2=${f,,}; mv "$f" "${f2// /-}"; done
Lri
  • 5,223