【问题标题】:Shell read *sometimes* strips trailing delimiterShell 读取 *有时* 会去除尾随分隔符
【发布时间】:2018-10-11 14:09:31
【问题描述】:

要解析冒号分隔的字段,我可以使用 read 和自定义 IFS

$ echo 'foo.c:41:switch (color) {' | { IFS=: read file line text && echo "$file | $line | $text"; }
foo.c | 41 | switch (color) {

如果最后一个字段包含冒号,没问题,冒号被保留。

$ echo 'foo.c:42:case RED: //alert' | { IFS=: read file line text && echo "$file | $line | $text"; }
foo.c | 42 | case RED: //alert

还保留了尾随分隔符...

$ echo 'foo.c:42:case RED: //alert:' | { IFS=: read file line text && echo "$file | $line | $text"; }
foo.c | 42 | case RED: //alert:

...除非它是 only 额外的分隔符。然后它被剥离。 等等,什么?

$ echo 'foo.c:42:case RED:' | { IFS=: read file line text && echo "$file | $line | $text"; }
foo.c | 42 | case RED

Bash、ksh93 和 dash 都这样做,所以我猜这是 POSIX 标准行为。

  1. 为什么会这样?
  2. 最好的选择是什么?

我想将上面的字符串解析为三个变量,并且不想破坏第三个字段中的任何文本。我曾认为read 是可行的方法,但现在我正在重新考虑。

【问题讨论】:

  • Stéphane Chazelas's answer on Unix.SE 回答了我的第一个问题的一部分,尽管我仍然不清楚这种行为是否合理或仅仅是一个丑陋的历史缺陷。
  • 我在spec for read 中没有看到任何表明应删除尾随字段分隔符的内容。
  • 您可以主动将包含: 的 cmets 附加到您的代码中。
  • 查看 bash 源代码builtins/read.def 我发现以下注释:Posix.2 表示最后一个变量获取剩余的单词及其中间的分隔符。 这只是在包含对函数 strip_trailing_ifs_whitespace 的调用的条件之前。

标签: bash shell sh


【解决方案1】:

是的,这是标准行为(参见read specificationField Splitting)。一些shell(至少基于ash,包括dashpdkshzshyash)过去不这样做,但zsh除外(当不在POSIX模式下时), busybox sh,其中大部分已更新为符合 POSIX 标准。

对于:

$ var='a:b:c:' IFS=:
$ set -- $var; echo "$#"
3

(请参阅read 的 POSIX 规范实际上如何遵循 字段拆分 机制,其中 a:b:c: 被拆分为 3 个字段,因此对于 IFS=: read -r a b c,字段数量变量)。

基本原理是ksh(POSIX 规范所基于)$IFS(最初在 Bourne shell 中,内部字段分隔符)变成了一个字段分隔符 em>,我认为可以表示任何元素列表(不包含分隔符)。

$IFS是一个分隔符时,一个不能代表一个空元素的列表(""被分割成一个0元素的列表,":"成一个两个元素的列表空元素¹)。当它是一个分隔符时,你可以用""表示一个零元素列表,或者用":"表示一个空元素,或者用"::"表示两个空元素。

这有点不幸,因为$IFS 最常见的用法之一是拆分$PATH。而像/bin:/usr/bin: 这样的$PATH 将被拆分为"/bin""/usr/bin""",而不仅仅是"/bin""/usr/bin"

现在,对于 POSIX shell(但并非所有 shell 在这方面都兼容),对于参数扩展时的分词,可以使用以下方法:

IFS=:; set -o noglob
for dir in $PATH""; do
  something with "${dir:-.}"
done

尾随 "" 确保如果 $PATH 以尾随 : 结尾,则会添加一个额外的空元素。并且空的$PATH 被视为一个空元素。

这种方法不能用于read

没有切换到zsh,除了插入一个额外的: 并在之后删除它之外没有简单的解决方法:

echo a:b:c: | sed 's/:/::/2' | { IFS=: read -r x y z; z=${z#:}; echo "$z"; }

或者(不太便携):

echo a:b:c: | paste -d: - /dev/null | { IFS=: read -r x y z; z=${z%:}; echo "$z"; }

我还添加了您在使用read 时通常需要的-r

最有可能在这里you'd want to use a proper text processing utility like sed/awk/perl instead of writing convoluted and probably inefficient code around readhas not been designed for that


¹ 尽管在 Bourne shell 中,它仍然被分成零个元素,因为那里 IFS 空白字符和 IFS 非空白字符之间没有区别,ksh 也添加了一些东西

【讨论】:

    【解决方案2】:

    read 的一个“功能”是它会在它填充的变量中使用strip leading and trailing whitespace separators - 在链接的答案中有更详细的解释。这使初学者可以让read 做他们期望做的事情,例如read first rest <<< ' foo bar '(注意额外的空格)。

    外卖?使用 Bash 和 shell 工具很难进行准确的文本处理。如果您想要完全控制,最好使用“更严格”的语言,例如 Python,split() 会做您想做的事,但您可能需要更深入地研究字符串处理以显式删除换行符分隔符或处理编码.

    【讨论】:

    • 好吧,那么①为什么当有多个尾随时它不去除尾随分隔符(例如IFS=: read a b c <<< "A:B:C:D:"; printf "[%s|%s|%s]\n" "$a" "$b" "$c"[A|B|C:D:])和②为什么空间处理方式与:不同时IFS=: (IFS=' ' read a b c <<< "A B C D "; printf "[%s|%s|%s]\n" "$a" "$b" "$c"[A|B|C D])?
    猜你喜欢
    • 2019-03-16
    • 2020-03-13
    • 1970-01-01
    • 1970-01-01
    • 2021-10-15
    • 1970-01-01
    • 1970-01-01
    • 2018-09-26
    • 1970-01-01
    相关资源
    最近更新 更多