【问题标题】:Bash: Split a string exactly like Readline would split itBash:拆分一个字符串,就像 Readline 拆分它一样
【发布时间】:2012-02-14 21:51:06
【问题描述】:

给定一个任意 Bash "simple command" 的字符串表示,我如何将它拆分为一个包含其各个“部分”的数组,即命令名称和各个参数,就像 shell 本身一样 ( ie Readline) 会在解析它并决定运行哪个可执行文件/函数以及传递哪些参数时将其拆分


我的特定用例需要解析用户定义的别名定义。例如。别名可能被定义为:

alias c2="cut -d' ' -f2"  # just an example... arbitrary commands should be handled!

这就是我的 bash 脚本尝试解析它的方式:

alias_name="c2"
alias_definition=$(alias -p | grep "^alias $alias_name=") # "alias c2='cut -d'\'' '\'' -f2'"
alias_command=${alias_definition##alias $alias_name=}     # "'cut -d'\'' '\'' -f2'"
alias_command=$(eval "echo $alias_command")               # "cut -d' ' -f2"

alias_parts=($alias_command) # WRONG - SPLITS AT EVERY WHITESPACE!

echo "command name: ${alias_parts[0]}"

for (( i=1; i <= ${#alias_parts}; i++ )); do
  echo "parameter $i : ${alias_parts[$i]}"
done

输出:

command name: cut
parameter 1 : -d'
parameter 2 : '
parameter 3 : -f2

期望的输出:

command name: cut
argument 1  : -d' '
argument 2  : -f2


我需要用什么替换 alias_parts=($alias_command) 行来实现这一点?

【问题讨论】:

    标签: string bash split readline


    【解决方案1】:

    正如 l0b0 所说,它不是 readline。是外壳本身在进行分裂。所以使用 shell 本身来做解析。

    alias c2="cut -d' ' -f2"
    
    split_parts() {
        alias_parts=("$@")
    }
    
    alias_defn=$(alias c2)
    # 2 evals needed to get rid of quotes
    eval eval split_parts ${alias_defn#alias c2=}
    
    for (( i=0; i < ${#alias_parts}; i++ )); do
      echo "parameter $i : \"${alias_parts[$i]}\""
    done
    

    输出

    parameter 0 : "cut"
    parameter 1 : "-d "
    parameter 2 : "-f2"
    

    请注意,-d 包含 shell 实际看到的尾随空格。

    【讨论】:

    • "正如 l0b0 所说,它不是 readline。" - 我认为是 Readline,因为在 Bash 参考中关于 COMP_WORDS 数组(恰好包含以这种方式拆分的行)的以下句子:“行被拆分为单词,因为 Readline 会拆分它”。
    • 这是可编程完成的一部分,由 readline 完成。 shell对命令的解析独立于readline,更多地基于IFS(正如其他一些海报指出的那样)。 readline 只是一个用于处理行编辑的精美库。
    【解决方案2】:

    set 内置可用于分割字符串。

    bash$ set -- cut -d ' ' -f2
    
    bash$ echo "'$3'"
    ' '
    

    编辑:如果要拆分的字符串已经在变量中,那就更麻烦了。您可能会玩弄eval,但在这种情况下,我会说这会使事情复杂化,而不是简化它们。

    bash$ a="cut -d ' ' -f2"
    
    bash$ eval set -- $a  # No quoting!
    
    bash$ echo "'$3'"
    ' '
    

    【讨论】:

    • 这接近我想要的。但是,除了拆分参数之外,它似乎还对各个参数执行字符串扩展,因此您得到的不是数组[cut,-d,' ',-f2],而是数组[cut,-d, ,-f2](在第三项中删除了引号)。是否有可能单独执行拆分步骤以保留原始参数?
    • 不,我不这么认为。我会选择性地在需要向用户显示的地方添加引号,但它们不是命令的正确部分,它们仅用于从 shell 中转义空格等。换句话说,[cut,-d, ,-f2] 正是您所需要的。
    • ... 但是,如果您的别名包含例如一个未加引号的通配符,在将其传递给 eval 之前需要对其进行转义。
    • 不幸的是,就我而言,[cut,-d,' ',-f2] 确实是我需要的,因为我试图在调用预先存在的 bash 完成之前将各个参数添加到自定义 bash 完成函数内的 COMP_WORDS 数组函数(特别是为别名命令定义的函数),我想将它们添加到数组中,就像如果别名中包含的完整命令将直接通过 TAB 完成,shell 本身会将它们添加到数组中一样.这恰好是没有扩展任何字符串或转义,而只是在参数边界处拆分。
    • 不,你误会了。引号不是值的一部分,它们的存在是为了防止它被替换,但是一旦变量中有空格,变量的值本身就不会(也不应该)包含引号。
    【解决方案3】:

    最小化“邪恶奥托”的解决方案:

    alias c2="cut -d' ' -f2"
    alias_definition=$(alias c2)
    eval eval alias_parts=( "${alias_definition#alias c2=}" )
    

    您可以使用 `declare -p' 进行快速数组打印:

    $ declare -p alias_parts
    declare -a alias_parts='([0]="cut" [1]="-d " [2]="-f2")'
    

    同样有用的可能是`printf %q'来引用一个参数“以一种可以被重用作为shell输入的方式”(来自:help printf):

    $ printf %q ${alias_parts[1]}
    -d\
    

    弗雷迪·沃尔托
    http://fvue.nl/wiki/Bash

    【讨论】:

    • 我需要将 printf 的第二个参数放在引号中以使其像这样工作:printf %q "${alias_parts[1]}"
    • 顺便说一句,与@tripleee 提出的“eval set --”方法相比,这种方法有明确的优势吗?
    【解决方案4】:

    这不是readline 拆分,而是getoptgetoptsFor example:

    params="$(getopt -o d:h -l directory:,help --name "$0" -- "$@")"
    
    eval set -- "$params"
    unset params
    
    while true
    do
        case "${1-}" in
            -d|--directory)
                directory="$2"
                shift 2
                ;;
            -h|--help)
                usage
                exit
                ;;
            --)
                shift
                if [ "${1+defined}" = defined ]
                then
                    usage
                fi
                break
                ;;
            *)
                usage
                ;;
        esac
    done
    

    【讨论】:

    • 不,我不是这个意思。即使没有 getopt/getopts,你也可以例如调用像./test.sh a b 'c d' 这样的bash 脚本,在脚本内部,参数$3 将被设置为'c d'。这就是我需要的那种拆分,除了脚本参数不需要它,而是手动将其应用于保存在变量中的字符串。
    【解决方案5】:

    如果我们将 alias_command 的每个参数放在自己的行上,然后(本地) 设置IFS=\n,我们完成了:

    parsealias ()
    {
       alias_command_spaces=$(eval "echo $(alias $1)" | sed -e "s/alias $1=//") # "cut -d' ' -f2"
       alias_command_nl=$(eval each_arg_on_new_line $alias_command_spaces)      # "cut\n-d' '\n-f2"
       local IFS=$'\n' # split on newlines, not on spaces
       alias_parts=($alias_command_nl) # each line becomes an array element, just what we need
       # now do useful things with alias_parts ....
    }
    

    现在我们只需要编写上面使用的命令each_arg_on_new_line,例如:

    #!/usr/bin/env perl
    
    foreach (@ARGV) {
      s/(\s+)/'$1'/g; # put spaces whithin quotes
      print "$_\n";
    }
    

    【讨论】:

      猜你喜欢
      • 2013-01-15
      • 1970-01-01
      • 1970-01-01
      • 2011-10-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-05
      相关资源
      最近更新 更多