2
\$\begingroup\$

Let's say that I have a directory that looks like this:

dir
    | File1.txt
    | fILE2.md
    | file3.tex

When I execute my bash script from within the dir directory, I want to receive files with names "file1.txt", "file2.md" and "file3.tex".

I have written this script:

ls | xargs -n1 -I {} sh -c 'fst={}; snd=`echo $fst | tr [:upper:] [:lower:]`; mv $fst $snd'

It works but is complex (uses variables). Is there a nicer way to perform it by piping commands? I don't want to use loops.

\$\endgroup\$
  • 2
    \$\begingroup\$ How about: rename 'y/A-Z/a-z/' * \$\endgroup\$ – AJNeufeld Sep 27 '19 at 5:19
  • \$\begingroup\$ Rename is not installed on the machine \$\endgroup\$ – Aemilius Sep 27 '19 at 5:26
  • \$\begingroup\$ Well, then install rename on your machine. This is code review, and the best code (as expanded upon in @Fólkvangr's answer) is the code which is clear, and easy to understand & maintain. This shouldn't be a programming challenge, where you must solve a problem with one hand tied behind your back. If this machine is under external management, make a case for to host to install rename. If they won't, find another hosting service. \$\endgroup\$ – AJNeufeld Sep 30 '19 at 17:12
5
\$\begingroup\$

The design constraint of "repeat (some action) without loops" is nonsensical.

Any solution that is not "type every rename command yourself" involves a loop. The loop may be hidden inside xargs and awk or rename; it's still there.

Anyway, assuming the files are like you say (well-formed, no embedded whitespace, all printable characters), you can have awk generate a shell script and feed that to a shell:

\ls | awk '/[A-Z]/ {print "mv",$1,tolower($1)}' | sh

If you need to deal with spaces and special characters, add quotes and use $0 (whole line) instead of $1 (first field):

\ls | awk '/[A-Z]/ {printf "mv '\''%s'\'\ \''%s'\''\n",$0,tolower($0)}'

If the filenames contain ' then this will fail and you need to work that out. Your code has the same problem. I won't get into the solution here because it involves a zillion backslashes and is the wrong way to solve this problem.

The right way to solve this problem is to use a tool built for the task (rename) or a shell loop. And adding -i is a good idea, in case the new filename already exists. This will work with any filename:

for f in *[A-Z]* ; do mv -i -- "$f" "${f,,}"; done
| improve this answer | |
\$\endgroup\$
3
\$\begingroup\$

Based on the answers provided by @Oh My Goodness and @AJNeufeld, I would say that you should use rename.

The most important issue is that your script is difficult to understand or hard to read. I recommend you read the chapter "Opening the Software Toolbox" which exposes the right approach to use the shell.

(An important additional point was that, if necessary, take a detour and build any software tools you may need first, if you don’t already have something appropriate in the toolbox.)

The best way to rename your filenames is to use rename because it is the right tool and the resulting command invocation is easy to read.

rename 'y/A-Z/a-z/' *

Note that the following command redirects the standard error of the command to a temporary file which has a "random" name. Therefore, you may know which files were not renamed. It may be useful if the information send to the standard error cannot be viewed on the screen (e.g. too much information displayed).

fn=$(mktemp -u); rename 'y/A-Z/a-z/' * 2>"$fn" && less -FX "$fn"

Eventually, you may test the command invocation before renaming files.

rename -n 'y/A-Z/a-z/' * >/dev/null

In conclusion, if you have the wrong tools you will do a bad job.

See also: Why you shouldn't parse the output of ls

| improve this answer | |
\$\endgroup\$
2
\$\begingroup\$

A shell script intended to be run as a command (i.e. not sourced into a running shell) should always have a shebang to specify the interpreter it should be run with. For , that would normally be

#!/bin/bash

That said, the script presented appears to be portable shell, so #!/bin/sh would be more appropriate as it stands.

There's no need for such long lines. A simple reformat gives a slightly more readable version:

#!/bin/sh

ls |
    xargs -n1 -I {} \
          sh -c 'fst={}; snd=`echo $fst | tr [:upper:] [:lower:]`; mv $fst $snd'

If we use Bash for the command, then we can use a downcasing expansion (${var,,}) instead of the cumbersome tr command:

#!/bin/sh

ls |
    xargs -n1 -I {} \
          bash -c 'fst={}; snd=${fst,,}; mv $fst $snd'

The output of ls isn't suitable for parsing. If we can assume GNU ls, then we could use --quoting-style to escape the names for reading by a shell:

#!/bin/sh

ls --quoting-style=shell-escape |
    xargs -n1 -I {} \
          bash -c 'fst={}; mv $fst ${fst,,}'

When we have names which differ only in case, do we really want the second to overwrite the first? I would suggest using mv -n to avoid overwriting existing files. Sadly this won't give any diagnostic that the file wasn't moved, though.

All this said, a simple for loop is a much simpler alternative:

#!/bin/bash

set -eu

for i in *
do mv -n -- "$i" "${i,,}"
done
| improve this answer | |
\$\endgroup\$

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.