【问题标题】:Extracting a string from a substring in bash (yes, that way around)从 bash 中的子字符串中提取字符串(是的,就是这样)
【发布时间】:2021-07-10 08:37:48
【问题描述】:

我在 bash 中有一个名为 comp_line 的字符串,其中可以包含任意数量的空格。例如:

"foo bar   apple  banana q xy"

我有一个从零开始的索引 comp_point 指向该字符串中的一个字符,例如如果comp_point 为 4,则指向 'bar' 中的第一个 'b'。

仅基于comp_pointcomp_line,我想提取索引指向的单词,其中“单词”是字母、数字、标点符号或任何其他非空白字符的序列,两边被空格包围(如果单词在字符串的开头或结尾,或者是字符串中唯一的单词,它应该以相同的方式工作。)

我要提取的单词将变为cur(当前单词)

基于此,我想出了一套规则:

读取当前字符curchar、前一个字符prevchar和下一个字符nextchar。那么:

  1. 如果curchar 是图形字符(非空格),请将cur 设置为curchar 之前和之后的字母(停止,直到您到达任一侧的空格或字符串开始/结束。)

  2. 否则,如果prevchar 是图形字符,则将cur 设置为前一个字母的字母,向后直到前一个空白字符/字符串开始。

  3. 否则,如果nextchar 是图形字符,则将cur 设置为从下一个字母开始的字母,直到下一个空白字符/字符串结束。

  4. 如果以上条件都没有满足(意思是curcharnextcharprevchar都是空白字符)设置cur""(空字符串)

我编写了一些我认为可以实现这一点的代码。规则 2、3 和 4 相对简单,但规则 1 是最难实现的——我不得不进行一些复杂的字符串切片。我不相信我的解决方案在任何方面都是理想的,并且想知道是否有人知道仅在 bash 中执行此操作的更好方法(而不是外包给 Python 或其他更简单的语言。)

https://rextester.com/l/bash_online_compiler上测试

#!/bin/bash
# GNU bash, version 4.4.20

comp_line="foo bar   apple  banana q xy"
comp_point=19
cur=""

curchar=${comp_line:$comp_point:1}
prevchar=${comp_line:$((comp_point - 1)):1}
nextchar=${comp_line:$((comp_point + 1)):1}
echo "<$prevchar> <$curchar> <$nextchar>"

if [[ $curchar =~ [[:graph:]] ]]; then
    # Rule 1 - Extract current word
    slice="${comp_line:$comp_point}"
    endslice="${slice%% *}"
    slice="${slice#"$endslice"}"
    slice="${comp_line%"$slice"}"
    cur="${slice##* }"
else
    if [[ $prevchar =~ [[:graph:]] ]]; then
        # Rule 2 - Extract previous word
        slice="${comp_line::$comp_point}"
        cur="${slice##* }"
    else
        if [[ $nextchar =~ [[:graph:]] ]]; then
            # Rule 3 - Extract next word
            slice="${comp_line:$comp_point+1}"
            cur="${slice%% *}"
        else
            # Rule 4 - Set cur to empty string ""
            cur=""
        fi
    fi
fi

echo "Cur: <$cur>"

当前示例将返回 'banana',因为 comp_point 设置为 19。

我确信一定有一种我没有想到的更简洁的方法,或者我错过了一些技巧。到目前为止它也有效,但我认为可能有一些我没有想到的极端情况。谁能告诉我是否有更好的方法?


(XY 问题,如果有人问的话)

我正在编写一个制表符完成脚本,并尝试使用 COMP_LINE 和 COMP_POINT 模拟 COMP_WORDS 和 COMP_CWORD 的功能。当用户键入命令以完成制表符时,我想仅根据后两个变量来确定他们尝试制表符完成的单词。我不想将此代码外包给 Python,因为当 Python 参与制表符完成时,性能会受到很大影响。

【问题讨论】:

  • 看到有人花这么多精力写一个问题真是令人耳目一新。谢谢你。但是,我想知道您为什么不首先使用COMP_CWORD。这可能是另一个/实际的 XY 问题吗?
  • 谢谢!更广泛的上下文是:bash 制表符补全有一个怪癖,它根据 COMP_WORDBREAKS 的内容将命令词拆分为 COMP_WORDS,其中包含冒号和其他几个字符。我正在为使用冒号编写完成的命令。我不得不设计一个迂回的解决方案,其中包括将COMP_WORDS 加入单个字符串并再次将其拆分为空格。但随后我需要正确识别用户光标的位置,如果用户选择的是空格字符,请将空字符串添加回 COMP_WORDS 的正确位置。

标签: string bash slice


【解决方案1】:

bash 中没有数组的另一种方式。

#!/bin/bash

string="foo bar   apple  banana q xy"

wordAtIndex() {
  local index=$1 string=$2 ret='' last first
  if [ "${string:index:1}" != " " ] ; then
    last="${string:index}"
    first="${string:0:index}"
    ret="${first##* }${last%% *}"
  fi
  echo "$ret"
}

for ((i=0; i < "${#string}"; ++i)); do
 printf '%s <-- "%s"\n' "${string:i:1}" "$(wordAtIndex "$i" "$string")"
done

【讨论】:

  • 谢谢。尽管我也对您的某些语法感到好奇。为什么在你的 if 条件下 [ ] 周围有括号?就此而言,人们通常不建议使用双方括号[[ ]] 来代替吗?还是在您的情况下偏好使用 single?
  • @Lou 你说得对,这里不需要括号。我更新答案。我通常在 [[ ]] 不可用的地方使用 sh 而不是 bash。这是一个简单的测试,所以这里不需要使用扩展的 bash 测试。
【解决方案2】:

如果有人知道仅在 bash 中执行此操作的更好方法

使用正则表达式。使用^.{4},您可以跳过前四个字母以导航到索引4。使用[[:graph:]]*,您可以匹配该索引处的其余单词。 * 很贪心,会匹配尽可能多的图形字符。

wordAtIndex() {
  local index=$1 string=$2 left right indexFromRight
  [[ "$string" =~ ^.{$index}([[:graph:]]*) ]]
  right=${BASH_REMATCH[1]}
  ((indexFromRight=${#string}-index-1))
  [[ "$string" =~ ([[:graph:]]*).{$indexFromRight}$ ]]
  left=${BASH_REMATCH[1]}
  echo "$left${right:1}"
}

这里是你的例子的完整测试:

string="foo bar   apple  banana q xy"
for ((i=0; i < "${#string}"; ++i)); do
  printf '%s <-- "%s"\n' "${string:i:1}" "$(wordAtIndex "$i" "$string")"
done

这会在左侧垂直输出输入字符串,并在每个索引上提取右侧索引指向的单词。

f <-- "foo"
o <-- "foo"
o <-- "foo"
  <-- ""
b <-- "bar"
a <-- "bar"
r <-- "bar"
  <-- ""
  <-- ""
  <-- ""
a <-- "apple"
p <-- "apple"
p <-- "apple"
l <-- "apple"
e <-- "apple"
  <-- ""
  <-- ""
b <-- "banana"
a <-- "banana"
n <-- "banana"
a <-- "banana"
n <-- "banana"
a <-- "banana"
  <-- ""
q <-- "q"
  <-- ""
x <-- "xy"
y <-- "xy"

【讨论】:

  • 谢谢!我还没有时间测试它,但它看起来是一个很好的解决方案。您的测试目前不符合我的要求 - 例如,“apple”之后的空格字符应该返回“apple”,而“banana”之前的空格字符应该返回“banana”。只有“bar”和“apple”之间的第二个空格应该返回一个空格,因为它之前和之后都有一个空格。但我确信我可以修改您的解决方案来解决我的要求。
  • 另外,我不明白双方括号[[]] 语法的作用。通常我会在 if 语句中使用它,或者使用 &amp;&amp;||。当你自己有双方括号时它会做什么?
  • 我刚刚意识到我很傻。您的解决方案解决了我原始解决方案的规则 1,这是我试图优化的主要内容。我仍然可以毫无问题地将我原来的解决方案用于规则 2、3 和 4。所以这应该很好用:)。谢谢!
  • @Lou 我很高兴这对你有用。关于没有if&amp;&amp;||[[ ]][[是一个内置命令,就像cdread一样。最常见的用例是基于其退出状态(例如if [[ ... ]][[ ... ]] &amp;&amp;),但不仅仅是退出状态。在我们的[[ string =~ ^.a(b*) ]] 案例中,变量BASH_REMATCH 已设置,允许我们仅提取匹配的部分内容。 BASH_REMATCH[1] 指的是群组(b*)
  • 谢谢!我做了更多的测试,最终弄清楚发生了什么。仍然有很多微妙的细微差别要抨击,我正在花时间弄清楚。
猜你喜欢
  • 2023-03-17
  • 2014-06-29
  • 2015-04-26
  • 2012-09-24
  • 2011-07-21
  • 1970-01-01
  • 1970-01-01
  • 2022-10-12
相关资源
最近更新 更多