bash - 在 bash 中有效获取命令输出的一致语法?

标签 bash shell return command-substitution

Bash 具有命令替换语法 $(f),它允许捕获 命令 f 的 STDOUT。如果该命令是可执行文件,那就没问题 – 无论如何,创建新流程是必要的。但如果命令是 shell 函数,使用此语法会产生大约 25ms 的开销 我系统上的每个子shell。这足以导致明显的延迟 当在内循环中使用时,尤其是在交互式上下文中,例如 命令完成或$PS1

常见的优化是使用 global variables instead [1]对于返回值, 但这是以可读性为代价的:意图变得不太清晰,并且 shell 函数和之间的输出捕获突然不一致 可执行文件。我在下面添加了选项及其弱点的比较。

为了获得一致、可靠的语法,我想知道 bash 是否有 任何允许捕获 shell 函数和可执行输出的功能 同样,同时避免使用 shell 函数的子 shell。

理想情况下,解决方案还应包含在子 shell 中执行多个命令的更有效的替代方案,这允许更干净地隔离问题,例如

person=$(
    db_handler=$(database_connect)    # avoids leaking the variable
    query $db_handler lastname        #   outside it's required
    echo ", "                         #   scope.
    query $db_handler firstname
    database_close $db_handler
)

这样的构造允许代码的读者忽略 $() 内部的所有内容,如果他们对 $person 的格式化细节不感兴趣。


选项比较

1。使用命令替换

person="$(get lastname), $(get firstname)"

缓慢,但可读且一致:一开始对读者来说并不重要 看看 get 是 shell 函数还是可执行文件。

2。所有函数都使用相同的全局变量

get lastname
person="$R, "
get firstname
person+="$R"

模糊了 $person 应该包含的内容。或者,

get lastname
local lastname="$R"
get firstname
local firstname="$R"
person="$lastname, $firstname"

但这非常冗长。

3。每个函数都有不同的全局变量

get_lastname
get_firstname
person="$lastname $firstname"
  • 作业更具可读性,但是
  • 如果某个函数被调用两次,我们将回到 (2)。
  • 设置变量的副作用并不明显。
  • 很容易意外地使用错误的变量。

4。使用全局变量,其名称作为参数传递

get LN lastname
get FN firstname
person="$LN, $FN"
  • 更具可读性,轻松允许多个返回值。
  • 与捕获可执行文件的输出仍然不一致。
  • 注意:对动态变量名称的赋值应使用 declare 完成 而不是eval:

    $VARNAME="$LOCALVALUE"            # doesn't work.
    declare -g "$VARNAME=$LOCALVALUE" # will work.
    eval "$VARNAME='$LOCALVALUE'"     #  doesn't work for *arbitrary* values.
    eval "$VARNAME=$(printf %q "$LOCALVALUE")"
                                      # doesn't avoid a subshell afterall.
    

[1] http://rus.har.mn/blog/2010-07-05/subshells/

最佳答案

如果您希望 shell 函数高效,则不能通过 stdout 返回结果。如果他们这样做了,就没有办法得到它,只能通过在子 shell 中运行该函数并通过内部管道捕获输出,而这些操作有点昂贵(在现代系统上需要几毫秒)。

当我专注于 shell 脚本并且需要最大限度地提高其性能时,我使用了一种约定,其中函数 foo 将通过变量 foo 返回其结果。即使在 POSIX shell 中您也可以执行此操作,并且它具有一个很好的属性,即它不会覆盖您的本地变量,因为如果 foo 是一个函数,那么您已经保留了该名称。

然后我有这个 bx_r getter 函数,它运行 shell 函数并将其输出保存到名称由第一个参数指定的变量中,或者如果第一个参数是,则将输出输出到 stdout一个非法变量名的单词(如果该单词恰好是一个空单词,则没有换行符,即 '')。

我对其进行了修改,以便它可以与命令或函数统一使用。

您不能使用内置类型来区分两者,因为 type 通过 stdout 返回其结果 => 您需要捕获该结果,这将再次施加 fork 惩罚。

所以,当我要运行函数 foo 时,我要做的是检查是否有相应的变量 foo (这可以捕获局部变量,但你会如果您将自己限制为正确命名的 shell 函数名称,则可以避免出现这种情况的可能性)。如果有,我假设这就是函数 foo 返回其结果的地方,否则我在 $() 中运行它,捕获其标准输出。

这是带有一些测试代码的代码:

bx_varlike_eh()
{
    case $1 in
        ([!A-Za-z_0-9]*) false;;
        (*) true;;
    esac
}
bx_r() #{{{ Varname=$1; shift; Invoke $@ and save it to $Varname if a legal varname or print it
{
    # `bx_r '' some_command` prints without a newline
    # `bx_r - some_command` (or any non-variable-character-containing word instead of -) 
    #           prints with a newline

    local bx_r__varname="$1"; shift 1
    local bx_r
    if ! bx_varlike_eh "$1" || eval "[ \"\${$1+set}\" != set ]"; then
        #https://unix.stackexchange.com/a/465715/23692
        bx_r=$( "$@" ) || return #$1 not varlike or unset => must be a regular command, so capture
    else
        #if $1 is a variable name, assume $1 is a function that saves its output there
        "$@" || return
        eval "bx_r=\$$1" #put it in bx_r
    fi
    case "$bx_r__varname" in
        ('') printf '%s' "$bx_r";;
        ([!A-Za-z_0-9]*) printf '%s\n' "$bx_r";;
        (*) eval "$bx_r__varname=\$bx_r";;
    esac
} #}}}

#TEST
for sh in sh bash; do
    time $sh -c '
    . ./bx_r.sh
    bx_getnext=; bx_getnext() { bx_getnext=$((bx_getnext+1)); }
    bx_r - bx_getnext
    bx_r - bx_getnext
    i=0; while [ $i -lt 10000 ]; do
        bx_r ans bx_getnext
        i=$((i+1)); done; echo ans=$ans
    '
    echo ====

    $sh -c '
    . ./bx_r.sh
    bx_r - date
    bx_r - /bin/date
    bx_r ans /bin/date
    echo ans=$ans
    '
    echo ====
    time $sh -c '
    . ./bx_r.sh
    bx_echoget() { echo 42; }
    i=0; while [ $i -lt 10000 ]; do 
        ans=$(bx_echoget)
        i=$((i+1)); done; echo ans=$ans 
    '
done
exit

#MY TEST OUTPUT

1
2
ans=10002
0.14user 0.00system 0:00.14elapsed 99%CPU (0avgtext+0avgdata 1644maxresident)k
0inputs+0outputs (0major+76minor)pagefaults 0swaps
====
Thu Sep  5 17:12:01 CEST 2019
Thu Sep  5 17:12:01 CEST 2019
ans=Thu Sep 5 17:12:01 CEST 2019
====
ans=42
1.95user 1.14system 0:02.81elapsed 110%CPU (0avgtext+0avgdata 1656maxresident)k
0inputs+1256outputs (0major+350075minor)pagefaults 0swaps
1
2
ans=10002
0.92user 0.03system 0:00.96elapsed 99%CPU (0avgtext+0avgdata 3284maxresident)k
0inputs+0outputs (0major+159minor)pagefaults 0swaps
====
Thu Sep  5 17:12:05 CEST 2019
Thu Sep  5 17:12:05 CEST 2019
ans=Thu Sep 5 17:12:05 CEST 2019
====
ans=42
5.20user 2.40system 0:06.96elapsed 109%CPU (0avgtext+0avgdata 3220maxresident)k
0inputs+1248outputs (0major+949297minor)pagefaults 0swaps

正如你所看到的,你可以获得统一的调用语法,同时加快速度 由于消除了捕获 ($()) 的需要,小型 shell 函数的执行量最多增加了约 14 次。

关于bash - 在 bash 中有效获取命令输出的一致语法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57804252/

相关文章:

java - 脚本和Java : How can i say to Java to put the output file into a new folder?

bash - 如何用 Unix Bash 脚本中的新行替换 "\n"字符串

java - Java 中的嵌套返回语句

delphi - 捕获用鼠标选择的文本

linux - 内部脚本失败后清理外部 Bash 脚本

bash - 设置下一个/上一个工作日

linux - bash 中 expr 的语法错误

c++ - 如何将以微秒为单位的纪元时间转换为可读格式

c - 将变量作为参数传递并获得所需的返回值与传递指针基本相同吗?

linux - Bash 退出状态在脚本中不起作用