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 /