如何编写 bash 函数来打印和 运行 命令,当命令的参数带有空格或要扩展的内容时

How to write bash function to print and run command when the command has arguments with spaces or things to be expanded

在 Bash 脚本中,我经常发现这种模式很有用,我首先打印我将要执行的命令,然后执行命令:

echo 'Running this cmd: ls -1 "$HOME/temp/some folder with spaces'
ls -1 "$HOME/temp/some folder with spaces"

echo 'Running this cmd: df -h'
df -h

# etc.

注意 echo 命令中的 单引号 以防止那里的变量扩展!我的想法是我想打印我正在 运行ning 的 cmd, 完全按照我输入的方式打印 运行 命令 ,然后 运行它!

如何将其封装到一个函数中?

将命令包装到标准 bash 数组中,然后打印并调用它,像这样,sort-of 有效:

# Print and run the passed-in command
# USAGE:
#       cmd_array=(ls -a -l -F /)
#       print_and_run_cmd cmd_array
# See:
# 1. My answer on how to pass regular "indexed" and associative arrays by reference:
#     and
# 1. My answer on how to pass associative arrays: 
print_and_run_cmd() {
    local -n array_reference=""
    echo "Running cmd:  ${cmd_array[@]}"
    # run the command by calling all elements of the command array at once
    ${cmd_array[@]}
}

对于像这样的简单命令,它工作正常:

用法:

cmd_array=(ls -a -l -F /)
print_and_run_cmd cmd_array

输出:

Running cmd:  ls -a -l -F /
(all output of that cmd is here)

但是对于更复杂的命令,它被破坏了!:

用法:

cmd_array=(ls -1 "$HOME/temp/some folder with spaces")
print_and_run_cmd cmd_array

期望的输出:

Running cmd: ls -1 "$HOME/temp/some folder with spaces"
(all output of that command should be here)

实际输出:

Running cmd:  ls -1 /home/gabriel/temp/some folder with spaces
ls: cannot access '/home/gabriel/temp/some': No such file or directory
ls: cannot access 'folder': No such file or directory
ls: cannot access 'with': No such file or directory
ls: cannot access 'spaces': No such file or directory

第一个问题,如您所见,$HOMERunning cmd: 行中得到了扩展,而这是不应该的,并且该路径参数周围的双引号已被删除, 第二个问题 是该命令实际上并不 运行.

如何解决这 2 个问题?

参考文献:

  1. 我的 bash 演示程序,我有这个 print_and_run_cmd 函数:https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/bash/argument_parsing__3_advanced__gen_prog_template.sh
  2. 我首先记录了如何通过引用传递 bash 数组,就像我在该函数中所做的那样:
    1. Passing arrays as parameters in bash
    2. How to pass an associative array as argument to a function in Bash?

后续问题:

  1. Bash: how to print and run a cmd array which has the pipe operator, |, in it

检查您的脚本 shellcheck:

Line 2:
    local -n array_reference=""
             ^-- SC2034 (warning): array_reference appears unused. Verify use (or export if used externally).
 
Line 3:
    echo "Running cmd:  ${cmd_array[@]}"
                        ^-- SC2145 (error): Argument mixes string and array. Use * or separate argument.
                        ^-- SC2154 (warning): cmd_array is referenced but not assigned.
 
Line 5:
    ${cmd_array[@]}
    ^-- SC2068 (error): Double quote array expansions to avoid re-splitting elements.

您可能想研究一下 https://github.com/koalaman/shellcheck/wiki/SC2068。我们修复了所有错误并得到:

print_and_run_cmd() {
    local -n array_reference=""
    echo "Running cmd:  ${array_reference[*]}"
    # run the command by calling all elements of the command array at once
    "${array_reference[@]}"
}

对我来说,在这种情况下通过引用传递数组很奇怪。我会传递实际值。我经常这样做:

prun() {
   # in the style of set -x
   # print to stderr, so output can be captured
   echo "+ $*" >&2
   # or echo "+ ${*@Q}" >&2
   # or echo "+$(printf " %q" "$@")" >&2
   # or echo "+$(/bin/printf " %q" "$@")" >&2
   "$@"
}
prun "${cmd_array[@]}"

How do I fix these 2 problems?

将 linters、格式化程序和静态分析工具(例如 shellcheck)合并到您的工作流程中,并检查它们指出的问题。

并引用变量扩展。这是 "${array[@]}".

您可以使用 DEBUG 陷阱实现您想要的:

#!/bin/bash
 
set -T
trap 'test "$FUNCNAME" = print_and_run_cmd || trap_saved_command="${BASH_COMMAND}"' DEBUG
print_and_run_cmd(){
    echo "Running this cmd: ${trap_saved_command#* }"
    "$@"
}
outer(){
    print_and_run_cmd ls -1 "$HOME/temp/some folder with spaces"
}
outer
# output ->
# Running this cmd: ls -1 "$HOME/temp/some folder with spaces"
# ...

这似乎也有效:

print_and_run_cmd() {
    echo "Running cmd:  "
    eval "$cmd"
}

cmd='ls -1 "$HOME/temp/some folder with spaces"'
print_and_run_cmd "$cmd"

输出:

Running cmd:  ls -1 "$HOME/temp/some folder with spaces"
(result of running the cmd is here)

但现在的问题是,如果我也想打印一个 expanded 版本的命令,以验证该部分是否正常工作,我不能,或者至少,不知道怎么办。

如果您有 Bash 4.4 或更高版本,此函数可能会满足您的要求:

function print_and_run_cmd
{
    local PS4='Running cmd: '
    local -
    set -o xtrace

    "$@"
}

例如,运行

print_and_run_cmd echo 'Hello World!'

产出

Running cmd: echo 'Hello World!'
Hello World!
    xtrace 选项打开时,
  • local PS4='Running cmd: ' 为 shell 打印的命令设置前缀。默认值为 + 。本地化是指函数returns.

    时自动恢复PS4之前的值
  • local - 导致对 shell 选项的任何更改在函数 returns 时自动还原。特别是,当函数 returns 时,它会导致下一行的 set -o xtrace 自动撤消。在 Bash 4.4.

    中添加了对 local - 的支持

    来自 man bash,在 local [option] [name[=value] ... | - ] 部分下(强调已添加):

    If name is -, the set of shell options is made local to the function in which local is invoked: shell options changed using the set builtin inside the function are restored to their original values when the function returns.

  • set -o xtrace(相当于set -x)导致shell打印命令,前面是PS4的扩展值,之前运行 他们。

    参见 help set

我很喜欢,所以我把它标记为正确。虽然它没有完全回答我原来的问题,所以如果出现另一个答案,我可能不得不改变它。 无论如何,请参阅@pjh 的回答或下面代码如何工作的完整解释,以及所有这些行的含义。我已经使用 man bashhelp set.

中的一些来源帮助编辑了该答案

不过,我想更改格式并提供更多示例,以表明命令中确实发生了变量扩展。我还想提供一个通过引用的版本,一个不通过引用的版本,这样你就可以选择你最喜欢的调用方式。

这是我的示例,显示了两种调用方式(print_and_run1 cmd_arrayprint_and_run2 "${cmd_array[@]}"):

#!/usr/bin/env bash

# Print and run the passed-in command, which is passed in as an 
# array **by reference**.
# See here for a full explanation: 
# USAGE:
#       cmd_array=(ls -a -l -F /)
#       print_and_run1 cmd_array
print_and_run1() {
    local -n array_reference=""
    local PS4='Running cmd:  '
    local -
    set -o xtrace
    # Call the cmd
    "${array_reference[@]}"
}

# Print and run the passed-in command, which is passed in as members
# of an array **by value**.
# See here for a full explanation: 
# USAGE:
#       cmd_array=(ls -a -l -F /)
#       print_and_run2 "${cmd_array[@]}"
print_and_run2() {
    local PS4='Running cmd:  '
    local -
    set -o xtrace
    # Call the cmd
    "$@"
}

cmd_array=(ls -1 "$HOME/temp/some folder with spaces")

print_and_run1 cmd_array
echo ""
print_and_run2 "${cmd_array[@]}"
echo ""

示例 运行 并输出:

eRCaGuy_hello_world/bash$ ./print_and_run.sh 
Running cmd:  ls -1 '/home/gabriel/temp/some folder with spaces'
file1.txt
file2.txt

Running cmd:  ls -1 '/home/gabriel/temp/some folder with spaces'
file1.txt
file2.txt