Bash: 如何像Readline一样精确地拆分字符串

4
给定一个任意 Bash "简单命令" 的字符串表示,我该如何将其拆分为包含其各个“部分”的数组,即命令名称和单独的参数,就像 shell 本身(即 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)这一行,才能实现这个目标?
5个回答

3
正如l0b0所说,这并不是readline。是shell本身在进行分割。因此,使用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会拆分它一样”。 - smls
这是可编程完成的一部分,由readline完成。Shell对命令的解析与readline无关,更多地基于IFS(正如其他帖子中指出的)。readline只是一个处理行编辑的高级库。 - evil otto

2
为了最小化“邪恶奥托”的解决方案:
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\

Freddy Vulto
http://fvue.nl/wiki/Bash


我需要将printf的第二个参数用引号括起来,以使其像这样工作:printf %q "${alias_parts[1]}" - smls
顺便问一下,这种方法与@tripleee提出的“eval set --”方法相比,是否有明显的优势? - smls

1

这不是 readline 分割,而是 getoptgetopts例如

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

1
不,那不是我的意思。即使没有 getopt/getopts,你仍然可以像这样调用一个 bash 脚本:./test.sh a b 'c d',在脚本内部,参数 $3 将被设置为 'c d'。这就是我需要的拆分方式,只是我不需要用于脚本参数,而是手动将其应用于保存在变量中的字符串。 - smls

1

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](在第三个术语中删除引号)。有没有办法仅执行拆分步骤,以保留原始参数? - smls
1
不,我不这么认为。我会有选择地添加引号,以便在向用户显示时需要,但它们并不是命令的正确部分,它们只用于从 shell 转义空格等。换句话说,[cut,-d, ,-f2] 正是你所需要的。 - tripleee
但是,如果您的别名包含未引用的通配符,那么在将其传递给eval之前,需要对其进行转义。 - tripleee
不幸的是,在我的情况下 [cut,-d,' ',-f2] 真的是我需要的,因为我正在尝试在调用预先存在的 bash 完成函数之前将单个参数添加到自定义 bash 完成函数中的 COMP_WORDS 数组中(具体来说,是为别名命令定义的那个函数),并且我希望以与 shell 自身在直接完成包含在别名中的完整命令时添加它们到数组中的方式相同的方式添加它们。这恰好是在不扩展任何字符串或转义的情况下进行参数边界拆分。 - smls
1
不,你误解了。引号不是值的一部分,它们只是为了防止被替换,但是一旦变量中有空格,变量的值本身就不包括引号(也不应该包括)。 - tripleee
是的,但是选项补全系统是一个特殊情况。每当调用补全函数时,shell会填充包含命令行的COMP_LINE变量,该命令行与已键入的完全相同,并且包含相同行分成的字段的COMP_WORDS数组。这些字段不包含每个参数的逻辑值,而是与已键入的命令行的相应片段完全相同。因此,为了不破坏期望此行为的补全函数,当手动修改这些变量时,应该完全模仿它。 - smls

0
如果我们将每个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";
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接