[Dclug] useful bash shell function

Michael Henry lug-user at drmikehenry.com
Thu Oct 7 06:24:00 EDT 2010


On 10/05/2010 10:17 AM, Timothy Ball wrote:
> On Mon, Oct 04, 2010 at 10:51:09PM -0400, James Ewing Cottrell 3rd wrote:
>> SPOILER:  function mkmv() { mkdir -p $1 && mv -t $*; }
>
> nope. want the dest dir to be last arg not first arg hence all the
> dancing
>
> original was something ghetto like:
> function mkmv { mkdir "$2"; mv "$1" "$2"; }

All,

I really like seeing this kind of topic on the mailing list.  I
find I always learn something new.  After a little searching, it
appears that the Bourne shell doesn't have a direct way to
access the final command-line argument, so the "eval" trick
below is the standard idiom for accessing that argument:

  eval "DEST=\${$#}"

Suppose there are 12 command-line arguments (for concreteness,
call them "file1" ... "file11" and "destdir"). They are
accessible as $1, $2, ..., $9, ${10}, ${11}, and ${12} (the
braces are optional unless, as Jim pointed out, the index
requires more than a single digit).  In addition, "$#" evaluates
to the number of arguments, "12" in this case.  So conceptually,
you'd like to get the last argument by using "${$#}", which
would ideally expand to "${12}" and then further expand to
"destdir".  This kind of double expansion isn't directly
supported directly, however, so "${$#}" doesn't quite work.
Therefore the idiom uses "eval" for a second pass of evaluation.
First, the outer "$" is escaped with "\" so it gets treated
literally on the first-pass evaluation, then the resulting
string is passed to "eval", which treats the string as a snippet
of Bash script to be evaluated, creating in effect a second-pass
evaluation.  The progression is as follows:

  eval "DEST=\${$#}"

  ==> evaluate string

  eval DEST=${12}

  ==> evaluate "DEST=${12}" as Bash script

  # Now, DEST="destdir"

This is a neat trick.  I've done similar things in other
programming languages, but I've not seen it used with shell
script, so this is a novelty I plan to remember.

Based on the various contributions from posters in this thread,
it looks like Tim's original Bash function can be boiled down to
the following, still preserving the argument ordering he
requires:

  function mkmv {
      eval "DEST=\${$#}"
      mkdir -p "$DEST" && mv "$@"
  }

The double-evaluation trick provides direct access to the final
command-line argument.  The use of double-quotes protects the
arguments in case they contain any embedded special characters
(such as spaces in the filenames).  "$@" gives access to all of
the command-line arguments as a list of individual arguments as
originally provided to the function.  The use of "&&" ensures
that the "mv" will not be attempted if the destination directory
doesn't exist or cannot be created via "mkdir -p".  I've tested
this function for some erroneous cases, and both "mkdir" and
"mv" seem to provide sufficient error messages on their own to
allow the script to delegate the error detection to them, though
it might be nice to print a usage message if the function is
invoked with no arguments:

  function mkmv {
      eval "DEST=\${$#}"
      (mkdir -p "$DEST" && mv "$@") || echo "Usage: mkmv ...."
  }

One adjustment I'd make, based on a personal preference, is to
put this functionality into a separate script rather than a
function, like this:

  ------ file ~/bin/mkmv --------
  #!/bin/sh

  eval "DEST=\${$#}"
  (mkdir -p "$DEST" && mv "$@") || echo "Usage: ...."
 
Separate scripts may be used with tools like "xargs" which
cannot use Bash functions.  In addition, environment variables
defined from a Bash function remain after the function has
exited, which can pollute the environment of the running Bash
session.  In contrast, variables defined within a separate
script are part of the script's environment, and they do not
leak into the parent's environment.  This latter problem can be
mitigated by choosing variable names that are unlikely to
collide with existing environment variables.  In the above
function, "DEST" could perhaps be named "mkmv_DEST" to reduce
the likelihood of collision.

Michael Henry



More information about the Dclug mailing list