Bash:如何消除循环生成的变量末尾的逗号

Bash:如何消除循环生成的变量末尾的逗号

我有以下 bash 脚本,它在同一行输出两个随机数。

#!/bin/bash

for i in 1 2; do
   unset var
   until [ "$var" -lt 10000 ] 2>/dev/null; do
      var="$RANDOM"
done
printf "%s," "${var/%,/}"
done

输出为:

5751,2129,

$var我正在尝试删除末尾的逗号,"${var/%,/}"以便可以使用 $var = 5751,2129 的输出。有人能帮忙吗?

答案1

在您像这样分配之后:var="$RANDOM"var变量将保存来自 扩展的字符串$RANDOM。在 Bash 中,它将扩展为从到$RANDOM范围内的十进制数。那里没有字符,因此尝试从 扩展中删除是没有意义的。032767,,$var

您在输出中观察到的逗号字符来自代码的这一部分:

printf "%s," "${var/%,/}"
# here    ^

该命令在循环的每次迭代中都被调用,因此每次迭代都会在输出中添加一个逗号字符。

已打印的内容无法取消打印。有细微差别:

  • 您可以将输出通过管道传输到过滤器,过滤器可能会删除其中的某些部分。过滤器可能位于脚本之外(例如,您调用the_script | the_filter);或者您可以将某些输出通过管道传输到部分脚本的输出被过滤到脚本内部的过滤器中。在后一种情况下,整个脚本的输出将被过滤;但脚本部分已打印的内容不会被取消打印;我的意思是到达过滤器。过滤器稍后会将其移除。
  • 如果您打印到终端,则可以使其用新数据覆盖先前输出的某些部分。有一些字符和序列可以移动光标;但它们仍然会到达终端。您几乎可以立即在视觉上隐藏先前的输出,但如果您将输出重定向到文件(或重定向到不理解所用序列的终端),那么您会发现它全部都在那里。

消除不需要的逗号的正确方法是首先不要打印它;或者在脚本中过滤它。有几​​种方法可以做到这一点,我无意找出所有方法。我将讨论其中的一些。我假设您还想知道可能的方法,例如对于具有两次以上迭代的循环;也许是对于事先不知道迭代次数的循环;也许是对于没有用数字枚举的循环;也许是对于可能永远不会结束的循环(例如而不是while truefor

注意:您使用了printf "%s," "${var/%,/}",它不会打印尾随换行符。如果可能,我将尝试复制此行为。

几种可能的方法:

  1. 循环内部不依赖于$i。您可以摆脱循环并使用两个单独的变量:

    unset var1
    until [ "$var1" -lt 10000 ] 2>/dev/null; do
       var1="$RANDOM"
    done
    unset var2
    until [ "$var2" -lt 10000 ] 2>/dev/null; do
       var2="$RANDOM"
    done
    printf '%s,%s' "$var1" "$var2"
    

    笔记:

    • 它不是干燥
    • 它的扩展性不佳。(如果你有会怎样for i in {1..100}?)
    • 如果事先不知道迭代次数,就会变得很麻烦。
  2. 您可以将当前代码放在管道中并过滤掉尾随的逗号。示例:

    for i in 1 2; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
    printf "%s," "$var"
    done | sed 's/,$//'
    

    笔记:

    • sed(或您使用的任何过滤器)可能会也可能不会处理循环产生的不完整行(未以换行符结尾的行)。如果是,则sed取决于实现。
    • 如果sed确实处理了它,它可能仍会以换行符终止其输出。
    • sed(或您使用的任何过滤器)可能无法处理过长的行。上面的特定代码生成了一条相当短的行,但一般来说(想象多次迭代)长度可能是一个问题。
    • sed作为面向行的工具,在处理之前必须读取整行。在这种情况下,它必须读取其整个输入。直到所有迭代完成之后,您才会从中获得任何东西。
    • 循环在子 shell 中运行。一般情况下,您可能希望它在主 shell 中运行,无论出于何种原因。
  3. 您可以将当前代码的输出捕获到变量中。最后,在扩展变量时删除尾随的逗号:

    capture="$(for i in 1 2; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
    printf "%s," "$var"
    done)"
    printf "%s" "${capture%,}"
    

    笔记:

    • 循环在子 shell 中运行。一般情况下,您可能希望它在主 shell 中运行,无论出于何种原因。
    • 代码在到达最后一个printf(循环外)之前都是静默的。直到所有迭代完成之后,您才会得到任何东西。
    • 一般来说,循环可以输出任意数量的字节。我认为 Bash 可以在变量中存储大量数据;然后,由于printf是内置函数,它可以大概printf "%s" "${capture%,}"无需击打即可处理命令行的长度限制。我还没有彻底测试过这一点,因为在我看来,在 shell 变量中存储大量数据无论如何都不是最佳做法。不过,如果您知道输出的长度有限,这种做法可能是合理的。(记录在案:上面的代码肯定会生成非常短的输出。)
    • Bash 无法将 NUL 字符存储在变量中(大多数 shell 都不能;zsh 可以)。此外,$()还会删除所有尾随换行符。这意味着您不能使用变量来存储随意的输出并在稍后准确重现。(记录:在上面的代码中,里面的片段$()不会生成 NUL 或尾随换行符。)
  4. 您可以让每次迭代都附加到某个变量,而不是捕获输出:

    capture=''
    for i in 1 2; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
    capture="$capture$var,"
    done
    printf "%s" "${capture%,}"
    

    笔记:

    • 代码在主 shell 中运行(而不是在子 shell 中)。
    • 将数据存储在变量中的限制(参见前面的方法)仍然适用。
    • 在 Bash 中,你可以使用 附加到变量capture+="$var,"。(注意:如果已为变量设置了整数属性,则=+表示“添加”,而不是“追加”
  5. 您可以检测最后一次迭代并使用不带以下内容的格式,

    # this example is more educative with more than two iterations
    for i in {1..5}; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
       if [ "$i" -eq 5 ]; then
          printf "%s" "$var"
       else 
          printf "%s," "$var"
       fi
    done
    

    笔记:

    • 无子壳。
    • 如果您事先不知道次数,检测最后一次迭代会更加困难。
    • 如果迭代一个数组(例如for i in "${arr[@]}"),那就更加困难了。
    • 每次迭代都会立即打印,您将按顺序获得输出。即使循环无限,这也能起作用。
  6. 您可以检测第一次迭代并使用不带 的格式,。请注意,您可以在原始代码中使用,%s而不是;然后您将得到而不是。通过此更改,上述任何避免或删除尾随逗号的方法都可以转换为避免或删除前导逗号的方法。最后一种方法变为:%s,,5751,21295751,2129,

    # this example is more educative with more than two iterations
    for i in {1..5}; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
       if [ "$i" -eq 1 ]; then
          printf "%s" "$var"
       else 
          printf ",%s" "$var"
       fi
    done
    

    笔记:

    • 无子壳。
    • 1如果始终从(或通常为固定的唯一字符串)开始,则检测第一次迭代很容易。
    • 但是如果你迭代一个数组,例如,就比较困难了for i in "${arr[@]}"。你不应该检查,因为数组后面if [ "$i" = "${arr[1]}" ]可能有一个元素与它相同。处理这个问题的一个直接方法是保留一个循环的索引(在循环之前,然后在每次迭代结束时加一)并根据测试它的值;不过我觉得这样的代码有点麻烦。"${arr[1]}"index=11
    • 每次迭代都会立即打印,您将按顺序获得输出。即使循环无限,这也能起作用。
  7. 你可以让它,本身来自一个变量。在循环中输入一个空变量,并,在每次迭代结束时将其设置为。这将有效地在循环结束时只改变一次值第一的迭代。例如:

    # this example is more educative with more than two iterations
    separator=''
    for i in {1..5}; do
       unset var
       until [ "$var" -lt 10000 ] 2>/dev/null; do
          var="$RANDOM"
       done
       printf "%s%s" "$separator" "$var"
       separator=,
    done
    

    笔记:

    • 无子壳。
    • 即使遍历数组也能很好地工作。
    • 每次迭代都会立即打印,您将按顺序获得输出。即使循环无限,这也能起作用。

    我个人认为这种方法非常优雅。

一般注意事项:

  • 每个片段都会生成不带尾随换行符的输出(带有或另一个过滤器的片段可能例外sed)。如果您需要整个输出形成正确终止的文本行,请在循环后运行printf '\n'(或仅运行)。echo
  • 您希望,它成为分隔符,而不是终止符。这意味着,如果$var迭代扩展为空字符串,则迭代次数为零的循环将生成与迭代次数为一次的循环相同的空输出。在我们的例子中,$var每次都会扩展为非空字符串,并且我们知道迭代次数超过零次;但在一般情况下,使用分隔符代替终止符可能会导致歧义。

相关内容