*为什么将一个命令作为参数传递给另一个命令是一个糟糕的设计?

*为什么将一个命令作为参数传递给另一个命令是一个糟糕的设计?

我希望能够包装命令,以便如果其输出不适合终端,它将自动通过寻呼机进行管道传输。

现在我正在使用以下 shell 函数(在 Arch Linux 下的 zsh 中):

export LESS="-R"

RET="$($@)"
RET_LINES="$(echo "${RET}" | wc -l)"

if [[ $RET_LINES -ge $LINES ]]; then
  echo "${RET}" | ${PAGER:="less"}
else
  echo "${RET}"
fi

但这并不能真正说服我。有没有更好的方法(在鲁棒性和开销方面)来实现我想要的?我也愿意接受 zsh 特定的代码,如果它能很好地完成工作的话。


更新:自从我问这个问题我发现一个答案$LINES它提供了一个更好(如果更复杂)的解决方案,它在将输出传输到之前缓冲大多数行,less而不是全部缓存。遗憾的是,这也不是真正令人满意,因为这两种解决方案都无法解决长而绕行的问题。例如,如果上面的代码存储在名为 的函数中pager_wrap,则

pager_wrap echo {1..10000}

将很长的一行打印到标准输出,而不是通过寻呼机进行管道传输。

答案1

我有一个为 POSIX shell 合规性编写的解决方案,但我只在 bash 中测试过它,所以我不确定它是否可移植。而且我不了解 zsh,所以我没有尝试使其对 zsh 友好。你可以通过管道将你的命令输入其中;将命令作为参数传递给另一个命令是一个糟糕的设计*

当然,解决这个问题的任何方法都需要知道终端有多少行和列。在下面的代码中,我假设您可以依赖LINESCOLUMNS环境变量(less查看)。比较靠谱的方法有:

  • 使用rows="${LINES:=$(tput lines)}"cols="${COLUMNS:=$(tput cols)}",作为美联社建议, 或者
  • 查看 的输出stty size。请注意,此命令必须将终端作为其标准输入,因此,如果它在脚本中,并且您要通过管道传输到脚本中,则必须说stty size <&1(在 bash 中)或stty size < /dev/tty.捕获其输出甚至更加复杂。

秘密成分:该fold命令将按照屏幕的方式断开长行,因此脚本可以正确处理长行。

#!/bin/sh
buffer=$(mktemp)
rows="$LINES"
cols="$COLUMNS"
while true
do
      IFS= read -r some_data
      e=$?        # 1 if EOF, 0 if normal, successful read.
      printf "%s" "$some_data" >> "$buffer"
      if [ "$e" = 0 ]
      then
            printf "\n" >> "$buffer"
      fi
      if [ $(fold -w"$cols" "$buffer" | wc -l) -lt "$rows" ]
      then
            if [ "$e" != 0 ]
            then
                  cat "$buffer"
            else
                  continue
            fi
      else
            if [ "$e" != 0 ]
            then
                  "${PAGER:="less"}" < "$buffer"
                  # The above is equivalent to
                  # cat "$buffer"   | "${PAGER:="less"}"
                  # … but that’s a UUOC.
            else
                  cat "$buffer" - | "${PAGER:="less"}"
            fi
      fi
      break
done
rm "$buffer"

要使用这个:

  • 将以上内容放入文件中;假设您称其为mypager.
  • (可选)将其放入您的搜索路径目录中;例如,$HOME/bin
  • 通过键入 使其可执行chmod +x mypager
  • ps ax | mypager在或 之类的命令中使用它ls -la | mypager
    如果您跳过了第二步(将脚本放入您的搜索路径的目录中),您必须这样做,其中ps ax | path_to_mypager/mypagerpath_to_mypager可以是相对路径,例如“.”。

*为什么将一个命令作为参数传递给另一个命令是一个糟糕的设计?

一、美学/遵守传统/Unix哲学

Unix 有一个哲学做一件事并把它做好。例如,如果程序要以某种方式显示数据(如寻呼机那样),那么它不应该调用生成数据的机制。这就是管道的用途。

没有多少 Unix 程序执行用户指定的命令或程序。让我们看看一些这样做的:

  • shell,如 好​​吧,运行用户指定的命令是 shell 的sh -c "command"
    工作;这是一件事外壳所做的。 (当然我并不是说shell是一个简单的程序。)
  • envnicenohupsetsidsu, 和sudo。这些程序有一些共同点——它们的存在都是为了在修改后的执行环境1下运行程序。它们必须按照它们的方式工作,因为 Unix 通常不允许您更改另一个进程的执行环境;你必须改变你自己的流程,然后fork和/或exec
    _______
    1 我正在使用这个短语执行环境 广义上,不仅指环境变量,还包括“ nice”值、UID和GID、进程组、会话ID、控制终端、打开文件、工作目录、umask值、ulimits、信号处理、alarm定时器等进程属性。 ETC。
  • 允许“shell escape”的程序。唯一想到的例子是vi/ vim,尽管我很确定还有其他例子。这些都是历史文物。它们早于窗口系统甚至作业控制;如果您正在编辑文件,并且想要执行其他操作(例如查看目录列表),则必须保存文件并退出编辑器才能返回 shell。现在,您可以切换到另一个窗口,或使用Ctrl+ Z(或键入:suspend)返回 shell,同时保持编辑器处于活动状态,因此 shell 转义可以说已经过时了。

我没有计算执行其他(硬编码)程序以利用其功能而不是重复它们的程序。例如,某些程序可能会执行diffsort。 (例如,有故事称,早期版本用于spell 获取sort -u文档中使用的单词列表,然后diff(或者也许comm)将该列表与字典单词列表进行比较,并识别文档中的哪些单词不存在。词典。)

二.时间问题

根据脚本的编写方式,该RET="$($@)"行在调用的命令完成之前不会完成。因此,在生成数据的命令完成之前,您的脚本无法开始显示数据。解决这个问题最简单的方法可能是将数据生成命令与数据显示程序分开(尽管还有其他方法)。

三.命令历史

  1. 假设您运行一些命令,其输出由显示过滤器处理,并且您查看输出,并决定将该输出保存在文件中。如果您输入了(作为假设的示例)

    ps ax | mypager
    

    然后你可以输入

    !:1 > myfile
    

    或按并适当编辑该行。现在,如果您输入

    mypager "ps ax"
    

    您仍然可以返回并将该命令编辑为ps ax > myfile,但它并不那么简单。

  2. 或者假设您决定下一步要跑步ps uax。如果你已经打字了ps ax | mypager,你可以这样做

    !:0 u!:*
    

    同样,对于mypager "ps ax",它仍然是可行的,但可以说更难。

  3. 另外,看看两个命令:ps ax | mypagermypager "ps ax"。假设您history在一小时后运行列表。 ISTM,您必须mypager "ps ax"仔细查看才能看到正在执行的命令是什么。

四.复杂命令/引用

  1. echo {1..10000}显然只是一个示例命令; ps ax也好不了多少。如果你只想做某事怎么办小的更现实一点,比如ps ax | grep oracle?如果您输入

    mypager ps ax | grep oracle
    

    它将运行mypager ps ax 并通过管道输出grep oracle。因此,如果 from 的输出ps ax有 30 行长, 即使 from 的输出只有 3 行,mypager也会调用。可能有一些例子会以更戏剧性的方式失败。lessps ax | grep oracle

    所以你必须做我之前展示的事情:

    mypager "ps ax | grep oracle"
    

    RET="$($@)"不能处理这个。当然,有一些方法可以处理类似的事情,但不鼓励这样做。

  2. 如果您要捕获其输出的命令行更加复杂怎么办?例如,

    命令1精氨酸1” |   命令2  '精氨酸2'$'精氨酸3'

    其中 参数包含空格、制表符$、、、、、、、、、、、、、、、、、、、甚至可能和的混乱组合。​像这样的命令可能很难直接在 shell 中正确输入。现在想象一下不得不做的噩梦|\<>*;&[]()`'"引用将其作为参数传递给mypager.

答案2

这就是-F选项的less用途,尽管您也需要使用该-X选项,否则它会将文本打印到具有该选项的终端上的备用屏幕(这意味着退出后将无法随时使用该选项less)。这在未来可能会改变,因为当前有一个增强请求,要求当文本适合一个屏幕时隐含 -X -F(303)RedHat 系统显然自 2008 年以来就已经为此提供了补丁(虽然它还没有上游(截至2017-09-14,我刚刚发送了一封邮件到[电子邮件受保护]关于那个))。

所以:

cmd | less -RXF

如果输出太长时您仍然想使用备用屏幕,那么您需要花点心思(在没有上述 RedHat 补丁的系统上):

page() {
  L=${LINES:-$(tput lines)} C=${COLUMNS:-$(tput cols)} \
    perl -Mopen=locale -MText::Tabs -MText::CharWidth=mbswidth -e '
      while(<STDIN>) {
        if ($pager) {
          print $pager $_;
        } else {
          chomp(my $line = $_);
          $line =~ s/\e\[[\d;]*m//g;
          $l += 1 + int(mbswidth(expand($line)) / $ENV{C});
          $buf .= $_;
          if ($l > $ENV{L}) {
            open $pager, "|-", "less", "-R", @ARGV or die "pager: $!";
            print $pager $buf;
          }
        }
      }
      print $buf unless $pager;' -- "$@"
}

用作:

cmd | page

或者

page < file
page -S < file...

(不是page file,它只是为了分页标准输入)。

我们试图通过剥离颜色转义序列、展开选项卡并计算宽度来猜测输出的长度,以便我们可以确定显示给定文本行的终端行数。

只要输出没有其他转义序列或控制/编码错误的字符,就应该可以工作。

另请注意与 RedHat 补丁的一个显着区别:对于单屏输出,输出不会经过less后处理(如反相视频中控制字符的渲染、用...^X挤压空行)。-s虽然这更接近这里所要求的,但在实践中可能不太理想。

您可能需要安装 Text::CharWidth 模块,该模块不是标准模块之一(libtext-charwidth-perlDebian 上的软件包)。

相关内容