2

I have a directory of files whose names I want to shorten:

(3) andrew@andrew Learning_Plans $ ls -al
total 580
drwxr-xr-x 2 andrew andrew  4096 Apr 10 21:40 .
drwxr-xr-x 7 andrew andrew  4096 Apr 10 16:46 ..
-rw-rw-rw- 1 andrew andrew 17825 Mar 25 14:18 Edexcel International GCSE Physics Chapter 10 Properties of Waves Learning Plan.docx
-rw-rw-rw- 1 andrew andrew 18472 Mar 25 14:19 Edexcel International GCSE Physics Chapter 11 The Electromagnetic Spectrum Learning Plan.docx
-rw-rw-rw- 1 andrew andrew 18692 Mar 25 14:19 Edexcel International GCSE Physics Chapter 12 Light Waves Learning Plan.docx
:
etc

I ran the following from the command line:

while read x; do echo cp \'$x\' $(echo $x | cut -b38- | tr ' ' '_'); done < <(find . -type f)

which produced what I expected:

cp './Edexcel International GCSE Physics Chapter 17 Energy Resources and Electricity Generation Learning Plan.docx' Chapter_17_Energy_Resources_and_Electricity_Generation_Learning_Plan.docx
cp './Edexcel International GCSE Physics Chapter 19 Solids, Liquids and Gases Learning Plan.docx' Chapter_19_Solids,_Liquids_and_Gases_Learning_Plan.docx
cp './Edexcel International GCSE Physics Chapter 28 Cosmology Learning Plan.docx' Chapter_28_Cosmology_Learning_Plan.docx
:
etc

However, removing the echo gives:

cp: target ‘Chapter_17_Energy_Resources_and_Electricity_Generation_Learning_Plan.docx’ is not a directory
cp: target ‘Chapter_19_Solids,_Liquids_and_Gases_Learning_Plan.docx’ is not a directory
cp: target ‘Chapter_28_Cosmology_Learning_Plan.docx’ is not a directory
:
etc

I'm guessing it's something to do with spaces in the file names, but I would have thought the single-quotes would have taken care of that?

I've tried doing a copy/paste of the output of the echo back into the terminal, and it runs OK! It just won't run in the while loop.

Versions:

(3) andrew@andrew Learning_Plans $ bash --version
GNU bash, version 4.3.11(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
(3) andrew@andrew Learning_Plans $ cat /etc/*release
DISTRIB_ID=LinuxMint
DISTRIB_RELEASE=17.3
DISTRIB_CODENAME=rosa
DISTRIB_DESCRIPTION="Linux Mint 17.3 Rosa"
NAME="Ubuntu"
VERSION="14.04, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
1

The issue is with the spaces in the filenames. The filenames are split on spaces. When cp gets more than two arguments, the last argument has to be a directory. It's not, so it complains.

To remove the Edexcel International GCSE Physics string from each filename and to convert spaces to underscores in a safe way, use (in bash)

for name in 'Edexcel International GCSE Physics '*.docx; do
    newname=${name#Edexcel International GCSE Physics }
    newname=${newname// /_}

    mv -i "$name" "$newname"
done

This iterates over all relevant files in the current directory and creates a new name in the variable newname by first deleting the known substring from the very start of the name, and then converting the remaining spaces into underscores. The old name is then renamed to the new name.

Change the mv to cp if you actually want to create copies of the files (as in your code).

Testing:

$ ls
Edexcel International GCSE Physics Chapter 10 Properties of Waves Learning Plan.docx
Edexcel International GCSE Physics Chapter 11 The Electromagnetic Spectrum Learning Plan.docx
Edexcel International GCSE Physics Chapter 12 Light Waves Learning Plan.docx

(the loop is run here)

$ ls
Chapter_10_Properties_of_Waves_Learning_Plan.docx
Chapter_11_The_Electromagnetic_Spectrum_Learning_Plan.docx
Chapter_12_Light_Waves_Learning_Plan.docx

Would you want to apply this recursively on a number of subdirectories:

find . -type f -name 'Edexcel International GCSE Physics *.docx' -exec sh -c '
    for pathname; do
        name=$(basename "$pathname")

        newname=${name#Edexcel International GCSE Physics }
        newname=${newname// /_}

        mv -i "$pathname" "$(dirname "$pathname")/$newname"
    done' sh {} +

Related:

  • OK, I get that cp will choke on filenames containing spaces, which is why I put the quotes in. What I'm still puzzled about is (1) why cp in the while loop seems to be ignoring the quotes, because the echo clearly shows only two names - the source with spaces (quoted) and the target without spaces, and (2) why a direct copy/paste of that echo output into the command line works? – Andrew Woodward Apr 11 at 11:54
  • @AndrewWoodward You inserted literal single quotes. These are interpreted as part of the actual value given to echo, which is why they showed in the output. See the difference between printf '%s\n' \'hello world\' and printf '%s\n' 'hello world'. – Kusalananda Apr 11 at 11:56
  • @AndrewWoodward The echo command outputted a correct command. That does not mean that the input to echo was a correct command. – Kusalananda Apr 11 at 12:20
  • 1
    Yeah, after a bit of testing it finally clicked! Duh... Thanks :) – Andrew Woodward Apr 11 at 12:38
  • Double quoting parameter expansions would have saved all of this trouble, as it would have prevented the word-splitting. – D. Ben Knoble Apr 11 at 13:09

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.