You have a collection of MP3 files that you would like to put in your MP3 player. But you have more music than can fit on your MP3 player. How can you load your player with music without having to baby-sit it by dragging and dropping files until it is full?
Use a shell script to keep track of the available space as it copies files onto the MP3 player, quitting when it is full.
1 #!/usr/bin/env bash 2 # cookbook filename: load_mp3 3 # Fill up my mp3 player with as many songs as will fit. 4 # N.B.: This assumes that the mp3 player is mounted on /media/mp3 5 # 6 7 # 8 # determine the size of a file 9 # 10 function FILESIZE () 11 { 12 FN=${1:-/dev/null} 13 if [[ -e $FN ]] 14 then 15 # FZ=$(ls -s $FN | cut -d ' ' -f 1) 16 set -- $(ls -s "$FN") 17 FZ=$1 18 fi 19 } 20 21 # 22 # compute the freespace on the mp3 player 23 # 24 function FREESPACE 25 { 26 # FREE=$(df /media/mp3 | awk '/^/dev/ {print $4}') 27 set -- $(df /media/mp3 | grep '^/dev/') 28 FREE=$4 29 } 30 31 # subtract the (given) filesize from the (global) freespace 32 function REDUCE () 33 (( FREE-=${1:-0})) 34 35 # 36 # main: 37 # 38 let SUM=0 39 let COUNT=0 40 export FZ 41 export FREE 42 FREESPACE 43 find . -name '*.mp3' -print | 44 (while read PATHNM 45 do 46 FILESIZE "$PATHNM" 47 if ((FZ <= FREE)) 48 then 49 echo loading $PATHNM 50 cp "$PATHNM" /media/mp3 51 if (( $? == 0 )) 52 then 53 let SUM+=FZ 54 let COUNT++ 55 REDUCE $FZ 56 else 57 echo "bad copy of $PATHNM to /media/mp3" 58 rm -f /media/mp3/$(basename "$PATHNM") 59 # recompute because we don't know how far it got 60 FREESPACE 61 fi 62 # any reason to go on? 63 if (( FREE <= 0 )) 64 then 65 break 66 fi 67 else 68 echo skipping $PATHNM 69 fi 70 done 71 printf "loaded %d songs (%d blocks)" $COUNT $SUM 72 printf " onto /media/mp3 (%d blocks free) " $FREE 73 ) 74 # end of script
Invoke this script and it will copy any MP3 file that it finds from the current directory on down (toward the leaf nodes of the tree) onto an MP3 player (or other device) mounted on /media/mp3. The script will try to determine the freespace on the device before it begins its copying, and then it will subtract the disk size of copied items so as to know when to quit (i.e., when the device is full, or as full as we can get it).
The script is simple to invoke:
$ fillmp3
and then you can watch as it copies files, or you can go grab a cup of coffee—it depends on how fast your disk and your MP3 memory writes go.
Let’s look at some bash features used in this script, referencing them by line number.
Let’s start at line 35, after the opening comments and the
function definitions. (We’ll return to the function definitions later.)
The main body of the shell script starts by initializing some variables
(lines 38–39) and exporting some variables so they will be available
globally. At line 42 we call the FREESPACE
function
to determine how much free space is available on the MP3 player before
we begin copying files.
Line 43 has the find command that will locate all the MP3 files (actually only those
files whose names end in “.mp3”). This information is piped into a
while
loop that begins on line
44.
Why is the while
loop
wrapped inside of parentheses? The parentheses mean that
the statements inside it will be run inside of a subshell. But what
we’re concerned about here is that we group the while
statement with the
printf statements that follow (lines 71 and 72).
Since each statement in a pipeline is run in its own subshell, and since
the find pipes its output into the while loop
, then none of the counting that we
do inside the while loop
will be
available outside of that loop. By putting the while
and the printfs
inside of a subshell, they are now both executing in the same shell
environment and can share variables.
Let’s look inside the while loop and see what it’s doing:
46 FILESIZE "$PATHNM" 47 if ((FZ <= FREE)) 48 then 49 echo loading $PATHNM 50 cp "$PATHNM" /media/mp3 51 if (( $? == 0 )) 52 then
For each filename that it reads (from the
find command’s output) it will use the FILESIZE
function
to determine the size of that file (see below for a discussion of that
function). Then it checks (line 47) to see if the file is smaller than
the remaining disk space, i.e., whether there is room for this file. If
so, it will echo the filename so we can see what
it’s doing and then it will copy (line 50) the file onto the MP3
player.
It’s important to check and see if the copy command completed
successfully (line 51). The $? is the result of the previous command, so
it represents the result of the the cp command. If
the copy is successful, then we can deduct its size from the space
available on the MP3 player. But if the copy failed, then we need to try
to remove the copy (since, if it is there at all, it will be
incomplete). We use the -f
option on
rm so as to avoid error messages if the file never
got created. Then we recalculate the free space to be sure that we have
the count right. (After all, the copy might have failed because somehow
our estimate was wrong and we really are out of space.)
In the main part of the script, all three of our if
statements
(lines 47, 51, and 63) use the double parentheses around the expression.
All three are numerical if
statements, and we wanted to use the familiar operators (vis. <= and ==). These same if
conditions could have been checked using the square bracket ([) form of
the if
statement, but then the
operators would be -le
and -eq
. We do use a different form of the
if
statement in line 13, in the
FILESIZE
function. There we need to
check the existence of the file (whose name is in the variable $FN
). That is simple to write with the
-e
operator, but that is not
available to the arithmetic-style if
statement (i.e., when using parentheses instead of square
brackets).
Speaking of arithmetic expressions, lets take a look at the REDUCE
function and
see what’s going on there:
32 function REDUCE ( ) 33 (( FREE-=${1:-0}))
Most people write functions using curly braces to delimit the body
of the function. However, in bash, any compound
statement will work. In this case we chose the double parentheses of
arithmetic evaluation, since that is all we need the function to do.
Whatever value is supplied on the command line that invokes REDUCE
will be the first (positional)
parameter (i.e., $1
). We simply
subtract that value from $FREE
to get
the new value for $FREE
. That is why
we used the arithmetic expression syntax—so that we can use the -=
operator.
While we are looking at the functions, let’s look at two lines in
the FILESIZE
function. Take a close
look at these lines:
16 set -- $(ls -s "$FN") 17 FZ=$1
There is a lot going on in those few characters. First, the
ls command is run inside of a subshell (the $()
construct). The -s
option on
ls gives us the size, in blocks, of the file along
with the file name. The output of the command is returned as words on
the command line for the set
command.
The purpose of the set
command here
is to parse the words of the ls output. Now there
are lots of ways we could do that, but this approach is a useful
technique to remember.
The set
-- will take the
remaining words on the command line and make them the new positional
parameters. If you write set --this is a
test
, then $1
is this
and $3
is a
. The previous values for
$1, $2
, etc are lost, but in line 12
we saved into $FN
the only parameter
that gets passed in to this function. Having done so, we are free to
reuse the positional parameters, and we use them by having the shell do
the parsing for us. We can then get at the file size as $1
, as you see in line 17. (By the way, in
this case, since this is inside a function, it is only the function’s
positional parameters that are changed, not those from the invoking of
the script.)
We use this technique of having the shell do our parsing for us, again on line 27 in the other function:
27 set -- $(df /media/mp3 | grep '^/dev/') 28 FREE=$4
The output of the df command will report on
the size, in blocks, available on the device. We pipe the output through
grep, since we only want the one line with our
device’s information and we don’t want the heading line that
df produces. Once bash has set
our arguments, we can grab the free space on the device as $4
.
The comment on line 26 shows an alternative way to parse the output of the df command. We could just pipe the output into awk and let it parse the output from df for us:
26 # FREE=$(df /media/mp3 | awk '/^/dev/ {print $4}')
By using the expression in slashes, we tell awk to pay attention only to lines with a leading /dev. (The caret anchors the search to the beginning of the line and the back-slash escapes the meaning of the slash, so as not to end the search expression at that point and to include a slash as the first character to find.)
So which approach to use? They both involve invoking an external program, in one case grep and in the other awk. There are usually several ways to accomplish the same thing (in bash as in life), so the choice is yours. In our experience, it usually comes down to which one you think of first.
3.23.101.60