Bourne Shell Scripting/Appendix D: Cookbook

From Wikibooks, open books for an open world
Jump to: navigation, search
Wikimedia Commons logo Post a new Cookbook entry
If you use the title box, then you do not need to put a title in the body.

Branch on extensions[edit]

When writing a bash script which should do different things based on the extension of a file, the following pattern is helpful.

 
 #filepath should be set to the name(with optional path) of the file in question
 ext=${filepath##*.}
 if [[ "$ext" == txt ]] ; then 
   #do something with text files
 fi

(Source: slike.com Bash FAQ).

Rename several files[edit]

This recipe shows how to rename several files following a pattern.

In this example, the user has huge collection of screenshots. This user wants to rename the files using a Bourne-compatible shell. Here is an "ls" at the shell prompt to show you the filenames. The goal is to rename images like "snapshot1.png" to "nethack-kernigh-22oct2005-01.png".

$ ls
snapshot1.png   snapshot25.png  snapshot40.png  snapshot56.png  snapshot71.png
snapshot10.png  snapshot26.png  snapshot41.png  snapshot57.png  snapshot72.png 
snapshot11.png  snapshot27.png  snapshot42.png  snapshot58.png  snapshot73.png
snapshot12.png  snapshot28.png  snapshot43.png  snapshot59.png  snapshot74.png
snapshot13.png  snapshot29.png  snapshot44.png  snapshot6.png   snapshot75.png
snapshot14.png  snapshot3.png   snapshot45.png  snapshot60.png  snapshot76.png
snapshot15.png  snapshot30.png  snapshot46.png  snapshot61.png  snapshot77.png
snapshot16.png  snapshot31.png  snapshot47.png  snapshot62.png  snapshot78.png
snapshot17.png  snapshot32.png  snapshot48.png  snapshot63.png  snapshot79.png
snapshot18.png  snapshot33.png  snapshot49.png  snapshot64.png  snapshot8.png
snapshot19.png  snapshot34.png  snapshot5.png   snapshot65.png  snapshot80.png
snapshot2.png   snapshot35.png  snapshot50.png  snapshot66.png  snapshot81.png
snapshot20.png  snapshot36.png  snapshot51.png  snapshot67.png  snapshot82.png
snapshot21.png  snapshot37.png  snapshot52.png  snapshot68.png  snapshot83.png
snapshot22.png  snapshot38.png  snapshot53.png  snapshot69.png  snapshot9.png
snapshot23.png  snapshot39.png  snapshot54.png  snapshot7.png
snapshot24.png  snapshot4.png   snapshot55.png  snapshot70.png

First, to add a "0" (zero) before snapshots 1 through 9, write a for loop (in effect, a short shell script).

  • Use ? which is a filename pattern for a single character. Using it, I can match snapshots 1 through 9 but miss 10 through 83 by saying snapshot?.png.
  • Use ${parameter#pattern} to substitute the value of parameter with the pattern removed from the beginning. This is to get rid of "snapshot" so I can put in "snapshot0".
  • Before actually running the loop, insert an "echo" to test that the commands will be correct.
$ for i in snapshot?.png; do echo mv "$i" "snapshot0${i#snapshot}"; done
mv snapshot1.png snapshot01.png
mv snapshot2.png snapshot02.png
mv snapshot3.png snapshot03.png
mv snapshot4.png snapshot04.png
mv snapshot5.png snapshot05.png
mv snapshot6.png snapshot06.png
mv snapshot7.png snapshot07.png
mv snapshot8.png snapshot08.png
mv snapshot9.png snapshot09.png

That seems good, so run it by removing the "echo".

$ for i in snapshot?.png; do mv "$i" "snapshot0${i#snapshot}"; done

An ls confirms that this was effective.

Now change prefix "snapshot" to "nethack-kernigh-22oct2005-". Run a loop similar to the previous one:

$ for i in snapshot*.png; do
> mv "$i" "nethack-kernigh-22oct2005-${i#snapshot}"
> done

This saves the user from typing 83 "mv" commands.

Long command line options[edit]

The builtin getopts does not support long options so the external getopt is required. (On some systems, getopt also does not support long options, so the next example will not work.)

eval set -- $(getopt -l install-opts: "" "$@")
while true; do
    case "$1" in
        --install-opts)
            INSTALL_OPTS=$2
            shift 2
            ;;
        --)
            shift
            break
            ;;
    esac
done

echo $INSTALL_OPTS

The call to getopt quotes and reorders the command line arguments found in $@. set then makes replaces $@ with the output from getopt

Another example of getopt use can also be found in the Advanced Bash Script Guide

Process certain files through xargs[edit]

In this recipe, we want to process a large list of files, but we must run one command for each file. In this example, we want to convert the sampling rates of some sound files to 44100 hertz. The command is sox file.ogg -r 44100 conv/file.ogg, which converts file.ogg to a new file conv/file.ogg. We also want to skip files that are already 44100 hertz.

First, we need the sampling rates of our files. One way is to use the file command:

$ file *.ogg
audio_on.ogg:            Ogg data, Vorbis audio, mono, 44100 Hz, ~80000 bps
beep_1.ogg:              Ogg data, Vorbis audio, stereo, 44100 Hz, ~193603 bps
cannon_1.ogg:            Ogg data, Vorbis audio, mono, 48000 Hz, ~96000 bps
...

(The files in this example are from Secret Maryo Chronicles.) We can use grep -v to filter out all lines that contain '44100 Hz':

$ file *.ogg | grep -v '44100 Hz'
cannon_1.ogg:            Ogg data, Vorbis audio, mono, 48000 Hz, ~96000 bps
...
jump_small.ogg:          Ogg data, Vorbis audio, mono, 8000 Hz, ~22400 bps
live_up.ogg:             Ogg data, Vorbis audio, mono, 22050 Hz, ~40222 bps
...

We finished with "grep" and "file", so now we want to remove the other info and leave only the filenames to pass to "sox". We use the text utility cut. The option -d: divides each line into fields at the colon; -f1 selects the first field.

$ file *.ogg | grep -v '44100 Hz' | cut -d: -f1
cannon_1.ogg
...
jump_small.ogg
live_up.ogg
...

We can use another pipe to supply the filenames on the standard input, but "sox" expects them as arguments. We use xargs, which will run a command repeatedly using arguments from the standard input. The -n1 option specifies one argument per command. For example, we can run echo sox repeatedly:

$ file *.ogg | grep -v '44100 Hz' | cut -d: -f1 | xargs -n1 echo sox
sox cannon_1.ogg
...
sox itembox_set.ogg
sox jump_small.ogg
...

However, these commands are wrong. The full command for cannon_1.ogg, for example, is sox cannon_1.ogg -r 44100 conv/cannon_1.ogg. "xargs" will insert incoming data into placeholders indicated by "{}". We use this strategy in our pipeline. If we have doubt, then first we can build a test pipeline with "echo":

$ file *.ogg | grep -v '44100 Hz' | cut -d: -f1 | \
> xargs -i 'echo sox {} -r 44100 conv/{}'
sox cannon_1.ogg -r 44100 conv/cannon_1.ogg
...
sox itembox_set.ogg -r 44100 conv/itembox_set.ogg
sox jump_small.ogg -r 44100 conv/jump_small.ogg
...

It worked, so let us remove the "echo" and run the "sox" commands:

$ mkdir conv
$ file *.ogg | grep -v '44100 Hz' | cut -d: -f1 | \
> xargs -i 'sox {} -r 44100 conv/{}'

After a wait, the converted files appear in the conv subdirectory. The above three lines alone did the entire conversion.

Simple playlist frontend for GStreamer[edit]

If you have GStreamer, the command gst-launch filesrc location=filename ! decodebin ! audioconvert ! esdsink will play a sound or music file of any format for which you have a GStreamer plugin. This script will play through a list of files, optionally looping through them. (Replace "esdsink" with your favorite sink.)

#!/bin/sh
loop=false
if test x"$1" == x-l; then
  loop=true
  shift
fi

while true; do
  for i in "$@"; do
    if test -f "$i"; then
      echo "${0##*/}: playing $i" > /dev/stderr
      gst-launch filesrc location="$i" ! decodebin ! audioconvert ! esdsink
    else
      echo "${0##*/}: not a file: $i" > /dev/stderr
    fi
  done
  if $loop; then true; else break; fi
done

This script demonstrates some common Bourne shell tactics:

  • "loop" is a boolean variable. It works because its values "true" and "false" are both Unix commands (and sometimes shell builtins), thus you can use them as conditions in if and while statements.
  • The shell builtin "shift" removes $1 from the argument list, thus shifting $2 to $1, $3 to $2, and so forward. This script uses it to process an "-l" option.
  • The substitution ${0##*/} gives everything in $0 after the last slash, thus "playlist", not "/home/musicfan/bin/playlist".