未命名管道中错误的控制序列

未命名管道中错误的控制序列
$ awk -v f=<(cmdmayfail) -e 'BEGIN { r = getline < f ; print r }'
-bash: cmdmayfail: command not found
0

在上面的无命名管道示例中,awk不会知道来自无命名管道的错误。

$ awk -v f=<(cmdmayfail || echo "control sequence" ) -e 'BEGIN { r = getline < f ; print r }'

为了awk意识到这个错误,我可以通过发送控制序列和一些错误信息来使用上面的代码。

鉴于file已经知道许多文件类型,是否有合理的控制序列可用于此应用程序,以便awk不会将错误控制序列误认为来自合法文件?谢谢。

答案1

如果您的commandmayfail命令是标准 Unix 命令,则在控制序列前面加上您自己的复杂文本(例如__ERROR__CMDFAIL__:)就足以让我们awk理解差异。

但是,如果您还包含自己的和/或专有软件,则很难为您提供通用字符串。您的某个专有命令可能(尽管可能性不大)使用这样的字符串。您应该查看错误消息的常规设置并创建一个不太可能使用的字符串。

如果commandmayfailfile,正如问题所暗示的那样,使用没有 的字符串可能就足够了:

答案2

如果 中的输出cmdmayfail相对较小,并且命令本身独立于代码的其他部分终止,则可以将其输出存储在变量中,并将退出状态作为第一行传递。 中的代码<()如下:

out="$(cmdmayfail)"; printf '%s\n' "$?" "$out"

awk应该getline<f获取退出状态。连续getline<f将读取的实际输出cmdmayfail

限制:

  • Bash 中的变量不能存储空字符。
  • $()将删除所有尾随换行符;然后printf只添加一个。避免这种情况的一个麻烦技巧是:

    out="$(cmdmayfail; s="$?"; printf X; exit "$s")"; printf '%s\n%s' "$?" "${out%X}"
    

cmdmayfail事实上,您在块中处理输出BEGIN可能表明您希望提前cmdmayfail终止。也许这个解决方案就足够了。


通常cmdmayfail甚至可能“无休止”地运行(即直到您终止它),并且您可能希望在处理(“无休止的”)stdin 时读取其输出awk。在这种情况下,上述解决方案将不起作用。

您可以在输出的每一行前面添加cmdmayfail一些固定的状态行(例如OK),最后添加一行退出状态cmdmayfail。中的代码<()如下:

 cmdmayfail | sed 's/^/OK\n/'; printf '%s\n' "${PIPESTATUS[0]}"

例子:

$ (printf '%s\n' foo "bar baz"; exit 7) | sed 's/^/OK\n/'; printf '%s\n' "${PIPESTATUS[0]}"
OK
foo
OK
bar baz
7

然后你的awk代码应该getline<f检查它是否是OK。如果是,下一行(getline<f再次)cmdmayfail肯定是来自。循环解析所有行,直到没有OK你期望的。然后是退出状态。

这将正常工作,除非cmdmayfail可能产生不完整的线。 例子:

$ (printf 'foo\nincomplete line'; exit 22) | sed 's/^/OK\n/'; printf '%s\n' "${PIPESTATUS[0]}"

根据具体实施sed,该工具可能

  1. 完全忽略不完整的行,或者
  2. 处理它并添加缺少的换行符,或者
  3. 按原样处理。

实际上你会

  1. 错过部分输出,或
  2. 不知道该线路不完整,或者
  3. 获取附有退出状态的行。

在 (3) 的情况下printf '\n%s\n' "${PIPESTATUS[0]}"可能会有帮助。如果最后一行cmdmayfail完整,它将生成一个额外的空行;这样您的awk代码就可以分辨出来。

考虑一下在行中间被强制终止的情况cmdmayfail。您可能不想解析不完整的行。问题是:要知道awk行形式是否cmdmayfail完整,您需要测试下一个(状态)行。为此实现有用的逻辑awk可能至少不方便。


尽快检测出不完整的线路是件好事,read在 Bash 中可以做到这一点缺点是read速度很慢(请记住 Bash 变量不能存储空字符)。示例解决方案:

# define this helper function in the main shell
encerr () { ( eval "$@" ) | (while IFS= read -r line; do printf 'C\n%s\n' "$line"; done; [ -n "$line" ] && printf 'I\n%s\n' "$line") ; printf 'E\n%s\n' "${PIPESTATUS[0]}"; }
# this part you want to put in <()
encerr cmdmayfail

然后你需要解码里面的自定义协议awk。每行都是成对的。(请参阅下面的示例,以更直观地了解协议。)

  1. 从一对()中读出第一行getline<f
  2. 将第一行存储在变量中(first=$0)。
  3. 从一对()中读出第二行getline<f
  4. 分析第一行($first)。
    • 如果是,C那么第二个(当前$0)是来自的一整行cmdmayfail,您可以解析它。
    • 如果是,I那么第二个是来自的不完整行cmdmayfail,您可能想要或可能不想解析它。期待E下一对。
    • 如果是E,那么第二个就是退出状态cmdmayfail。您不应该期待进一步的配对。
  5. 环形。

注意我eval "$@"在函数内部使用了。你后面写的内容encerr将被第二次评估,因此通常你会想运行类似

encerr 'cmd1 -opt foo'

或者

encerr "cmd1 -opt foo"

甚至

encerr 'cmd1 -opt foo | cmd2'

基本上,这是用来运行远程命令的形式ssh。比较:

ssh a@b 'cmd1 -opt foo | cmd2'

或者你可以像这样构建函数:

encerr () { "$@" | …

像这样调用它:

encerr cmd1 -opt foo

相比于sudo

sudo cmd1 -opt foo

示例(使用原始函数eval):

  • 成功但输出为空

    $ encerr true
    E
    0
    
  • 失败并输出为空

    $ # I'm redirecting stderr so "command not found" doesn't obfuscate the example
    $ encerr nonexisting-command-foo1231234 2>/dev/null
    E
    127
    
  • 完成线路后成功

    $ encerr 'date; sleep 1; date'
    C
    Mon Sep 30 09:07:40 CEST 2019
    C
    Mon Sep 30 09:07:41 CEST 2019
    E
    0
    
  • 完成线路后发生故障

    $ encerr 'printf "foo\nbar\n"; false'
    C
    foo
    C
    bar
    E
    1
    
  • 不完整线路后的成功

    $ encerr 'printf "foo bar\n89 baz"'
    C
    foo bar
    I
    89 baz
    E
    0
    
  • 线路不完整导致的故障

    $ encerr 'printf "\nThe first line was empty and this one was interru"; exit 33'
    C
    
    I
    The first line was empty and this one was interru
    E
    33
    

相关内容