1

I need to make a shell script called chExt.sh. that accepts a file extension and then multiple files. It then needs to change the file extensions or state that the file does not exist.

./chExt.sh txt ocelot.cpp ../otherFolder/file.H cat.dog.TXT king cobra.dat is the general idea of the auto testers test. The problem is that I haven't the faintest clue how to deal with the one file with a space in it's name.

my code is:

newExt="$1"
shift
for x in $*
do
    fileName="$x"
    if test -f "$fileName";
    then
        name=$(echo "$fileName" | rev | cut -f 2- -d '.' | rev)
        newName="$name.$newExt"
        if test "$newName" != "$fileName";
        then
            mv "$x" "$newName"
        fi
    else
        echo "$fileName: No such file"
    fi
done
7
  • The file with spaces in its name must be escaped in the argument. king\ cobra.dat or "king cobra.dat"
    – chaos
    Commented Dec 4, 2015 at 6:43
  • I fixed the missing quotes in your code. Since you're not mentioning syntax errors, I assumed they were just copying errors and your actual code ios correctly quoted.
    – terdon
    Commented Dec 4, 2015 at 8:47
  • I'm confused ... $() is kind of a Bashism ... at least not supported in pure POSIX shells. Why don't you make use of string substitution to cut off the extension (${i%.*}) and of "$@" in the for loop? After all you seem to have some shell available which allows this. Commented Dec 4, 2015 at 8:54
  • @0xC0000022L it is most certainly not a bashism! It's POSIX, just not supported by some older shells.
    – terdon
    Commented Dec 4, 2015 at 9:19
  • @terdon: so where did it originate then? Not in the Bourne shell or the C shell to my knowledge. I wrote "kind of" intentionally to signify that this would be more commonly seen in Bash or Bash-inspired shells rather than in the stock sh on any given system. AFAIK a lot of features were pioneered and spread by particular shells and later standardized in SUS. Commented Dec 4, 2015 at 15:55

2 Answers 2

4

If you don't quote the file name, the shell has no way of knowing that foo bar.txt is one parameter and not two (foo and bar.txt). You therefore need to call your script like this:

./chExt.sh txt ocelot.cpp ../otherFolder/file.H cat.dog.TXT "king cobra.dat"

The next problem is that when variables are expanded, the resulting value is split on whitespace (or whatever the $IFS variable is set to). So, when you write for i in $*, the $* is expanded and split on whitespace. Therefore, $i becomes king for one iteration and cobra.txt for the next. The way around that is to quote the variable you want to expand so that it is not split.

Which brings us to the next issue which is the difference between $* and $@. If you use "$*", the parameters will all be treated as one long string:

$ cat foo.sh
#!/bin/sh
for i in "$*"; do
    echo "$i"
done
$ foo.sh foo "bar baz"
foo bar baz

Compare the above to:

#!/bin/sh
for i in "$@"; do
    echo "$i"
done
$ foo.sh foo "bar baz"
foo
bar baz

As you can see, using "$@" has the desired effect, each input parameter is treated separately, including the one with spaces.

As a final note, your script will also choke on file names starting with - and a letter corresponding to an option for echo (try with a file called -new, for example). A better way to strip the extension is to use the shell's native string manipulation features. ${var%.*} removes the shortest string matching a . and 0 or more characters from the end of the variable. In other words, the extension. Then, you also need to add -- to mv to indicate the end of option and the start of parameters so that it too can deal with file names starting with - (see Guideline 10 here).

So, an improved and working version of your script would be:

#!/bin/sh
newExt="$1"
shift
for fileName in "$@"
do
    if test -f "$fileName";
    then
    #    name=$(echo "$fileName" | rev | cut -f 2- -d '.' | rev)
    name=${fileName%.*}
        newName="$name.$newExt"
        if test "$newName" != "$fileName";
        then
            mv -- "$fileName" "$newName"
        fi
    else
        printf '%s: No such file\n' " $fileName"
    fi
done

Further reading:

3
  • The for i do syntax (instead of for i in "$@"; do) removes the danger of forgetting to quote the $@.
    – Kusalananda
    Commented Nov 23, 2018 at 16:25
  • @Kusalananda hmm, yes good point. I personally never use the for i do syntax because I find the explicit mention of "$@" more clear and readable (OK, also because I simply didn't know about for i do until a couple of years ago :P). But yes, you're quite right.
    – terdon
    Commented Nov 23, 2018 at 16:34
  • As far as I understand, it's a relatively recent addition to the POSIX shell grammar (but I don't know how recent, it may still be quite old). I'm sure someone in the POSIX loop knows exactly when it was included.
    – Kusalananda
    Commented Nov 23, 2018 at 16:40
1

Another noteworthy quality about "$*" and "$@" is that they represent the shell array - the current set of the shell's arguments. These are changeable with the set built-in, but are more generally the default operational array for any built-in array functions - to include the for array.

: "${2?no file arguments!}"
for x 
do     if    [ "${2+:}" ]
       then  set ".$1"
       else  [ ".${x##*.}" != "$1" ] &&
             [ -f "$x" ] &&
             mv -- "$x" "${x%.*}$1"
       fi
done

That's a pretty close approximation to what your script does. It is a little different in that it doesn't do the echo for an argument which is not a regular file. I'm not certain you're doing what it is you think you are there, though: you're checking if your argument is an existing, accessible, regular file, but you're echoing a message to stdout which says argument: No such file. On a unix-like system everything is a file: a pipe is a file, a directory is a file, a device is a file. And so your test and your message don't align. What's more, if you asked mv to move a non-existent file, it would print your message for you, and to stderr - all on its own.

Another thing I notice is that you're testing if your newname equals your old name, and I assume this is to avoid mv complaining about moving a file over itself. As it happens, mv has an option for quieting that.

I think I would do your thing like:

: "${2?no file arguments!}"
for x 
do     [ "${2+:}" ] &&
       set -- ".$1" && continue
       set -- "${x%.*}$1" "$1"
{      [  -e "$1" ] && printf "%s: exists!\n" "$1"; } ||
{    ! [  -f "$x" ] && printf "%s: not a regular file.\n" "$x"; } ||
       mv -f --    "$x" "$1"
       shift
done   >&2

This site is temporarily in read-only mode and not accepting new answers.

Not the answer you're looking for? Browse other questions tagged .