Home > Blockchain >  Are there lisp-like macros in the shell?
Are there lisp-like macros in the shell?

Time:10-25

I have a set of shell commands that look like this

if check-some-condition $a then;
   do stuff
   run-exit-code $a
fi

where check-some-condition and run-exit-code could be replaced by functions taking a single argument $a, while do stuff is a placeholder for possibly several shell commands. Is it possible to emulate the Lisp functionality of a macro where I could just write

(my-macro $a stuff)

and have it replaced by the code above? I am using Bash but I can use any other shell if they have features that make this easier. I thought at first of using functions but I don't think I can pass in a block of commands.

CodePudding user response:

There isn't a macro definition system in the shell, and consequently shell syntax is not walked in order to expand macros. However, the shell has a textual eval command. You can write a function which synthesizes shell syntax, such as by inserting arguments it has been given, into a template. The function can print that syntax, which the caller can capture using $(...) command substitution syntax and pass to eval:

eval $(macro-like foo bar)

The expansion will happen every time that line of code is executed.

I've done something like this on a very small number of occasions. I don't remember all the details, but I remember that the code was also taking advantage of Bash local variables, which have dynamic scope, like ancient Lisp dialects and defvar variables in Common Lisp.

In Bash, eval takes place in a dynamic environment which sees the surrounding local variables, which is something you can exploit; it can help bring about some macro-like semantics. In a Lisp with lexically scoped local variables, eval-ed code has no access to those variables, but macro-substituted code does. Under dynamic scope, evaled code can access and assign surrounding locals.

Here is an example. Note that because eval is a command which has no control over expansions taking place in its argument space because it is called (analogously to eval in Lisp being a function which doesn't control argument evaluation), the client code is encumbered with quoting responsibilities.

# $1 = variable
# $2 = low
# $3 = high
# $4 = body
dofor()
{
   cat <<!
$1=$2 ;
while [ \$$1 -lt $3 ] ; do
  $4
  $1=\$(( $1   1 ))
done
!
}

eval "$(dofor i 0 100 'printf "[%d]\n" $i')"

We could make it so that

eval $(dofor i 0 100 'printf "[%d]\n" $i')

works without the quotes, at the cost of more heaps of arcane escapery inside dofor.

Imagine we extended the shell with a built-in command evalcmd, which let us write this instead of the above:

evalcmd dofor i 0 100 'printf "[%d]\n" $i'

Can we write that as a shell function? It turns out, yes:

# run the command specified in the arguments
# capturing its output, which is evaled in quotes
evcmd()
{
  eval "$("$@")"
}

evcmd dofor i 0 100 'printf "[%d]\n" $i'

Now, though still monstrously inefficient, it's substantially more ergonomic.

Finally, let's ask: could we split dofor into an a dofor_impl which generates the code, and a dofor command which calls dofor_impl and invokes the evcmd semantics? Also, yes:

dofor_impl()
{
   cat <<!
$1=$2 ;
while [ \$$1 -lt $3 ] ; do
  $4
  $1=\$(( $1   1 ))
done
!
}

dofor()
{
  # like evcmd, but inserting an operator into the left position
  eval "$(dofor_impl "$@")"
}

dofor i 0 100 'printf "[%d]\n" $i'

This is not bad for some simple uses, but what we can't achieve is not having to put the $i into a quote so that the substitution doesn't take place before dofor is invoked.

CodePudding user response:

In Bash you can define functions as follows:

function run_exit_code () {
    echo "EXIT $1"
}

function check_some_condition () {
    echo "CHECKING $1";
    true
}

And your code can execute commands associated with variables:

function my_code () {
    var=$1
    stuff=$2
    if check_some_condition $var; then
        echo "OK";
        $stuff;
        run_exit_code $var
    fi
}

So you can write, for example:

$ my_code /tmp 'ls /'
CHECKING /tmp
OK
bin  boot  cdrom  dev  etc  home  lib  lib32  lib64  libx32  lost found  media  mnt  opt  proc  root  run  sbin  srv  swapfile  sys  tmp  usr  var
EXIT /tmp

If you want stuff to refer to $var, then you need to add eval:

function my_code () {
    var=$1
    stuff=$2
    if check_some_condition $var; then
        echo "OK";
        eval $stuff;                 # <<< eval
        run_exit_code $var
    fi
}

This allows you to write a quoted bash expression and have it beeing evaluated in the context of your function:

$ my_code / 'ls $var'
CHECKING /
OK
bin  boot  cdrom  dev  etc  home  lib  lib32  ...
EXIT /
  • Related