shell中是否有类似于lisp的宏?

zz2j4svz  于 2022-11-16  发布在  Shell
关注(0)|答案(2)|浏览(188)

我有一组shell命令,如下所示

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

其中check-some-conditionrun-exit-code可以用只带一个参数$a的函数来代替,而do stuff可能是几个shell命令的占位符。

(my-macro $a stuff)

用上面的代码替换它吗?我用的是Bash,但是我可以用任何其他的shell,如果他们有使这更容易的特性的话。我一开始想用函数,但是我不认为我可以传入一个命令块。

e3bfsja2

e3bfsja21#

在shell中没有宏定义系统,因此shell语法不能被用来扩展宏。但是,shell有一个文本的eval命令。你可以写一个函数来合成shell语法,比如通过将给定的参数插入到模板中。该函数可以打印该语法。调用者可以使用$(...)命令替换语法来捕获该命令并将其传递给eval

eval $(macro-like foo bar)

每次执行该代码行时都会发生扩展。
我在很少的情况下做过类似的事情,我不记得所有的细节,但我记得代码也利用了Bash local变量,它有动态作用域,就像古老的Lisp方言和Common Lisp中的defvar变量。
在Bash中,eval发生在一个动态环境中,它可以看到周围的local变量,这是可以利用的;它可以帮助产生一些类似于宏的语义。在具有词法作用域的局部变量的Lisp中,eval-ed代码不能访问这些变量,但宏替代的代码可以。在动态作用域下,evaled代码可以访问和赋值周围的局部变量。
这里有一个例子,注意,由于eval是一个命令,它不能控制其参数空间中发生的扩展,因为它被调用了(类似于Lisp中的eval是一个不控制参数求值的函数),客户端代码被引用的责任所拖累。

# $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')"

我们可以让它

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

没有引号的作品,代价是在dofor内部有更多的神秘逃避。
假设我们用内置命令evalcmd扩展了shell,这样我们就可以编写如下代码而不是上面的代码:

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

我们能把它写成一个shell函数吗?结果是,是的:

# 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'

现在,虽然仍然是可怕的效率,它大大更符合人体工程学。
最后,让我们问:我们是否可以将dofor拆分为一个生成代码的dofor_impl和一个调用dofor_impl并调用evcmd语义的dofor命令?同样,可以:

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'

这对于一些简单的用法来说是不错的,但是我们不能实现的是不必将$i放在引号中,这样在调用dofor之前就不会发生替换。

6fe3ivhb

6fe3ivhb2#

在Bash中,您可以定义函数,如下所示:

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

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

并且您的代码可以执行与变量关联的命令:

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

所以你可以写,例如:

$ 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

如果您希望stuff引用$var,则需要添加eval

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

这样就可以编写一个带引号的bash表达式,并在函数的上下文中对其进行求值:

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

相关问题