如果(且仅当)太长时,通过寻呼机通过管道传输命令输出的最佳方法是什么?

A.P*_*.P. 11 command-line shell less zsh

我希望能够包装一个命令,这样如果它的输出不适合终端,它将自动通过寻呼机传送。

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

export LESS="-R"

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

if [[ $RET_LINES -ge $LINES ]]; then
  echo "${RET}" | ${PAGER:="less"}
else
  echo "${RET}"
fi
Run Code Online (Sandbox Code Playgroud)

但这并不能真正说服我。有没有更好的方法(在健壮性和开销方面)来实现我想要的?我也对 zsh 特定的代码持开放态度,如果它可以很好地完成工作。


更新:自从我问了这个问题后,我找到了一个答案,它提供了一个更好的(如果更复杂)解决方案,它在$LINES将输出输送到less而不是全部缓存之前缓冲大多数行。可悲的是,这也不是很令人满意,因为这两种解决方案都没有考虑过长的包裹线。例如,如果上面的代码存储在一个名为 的函数中pager_wrap,那么

pager_wrap echo {1..10000}
Run Code Online (Sandbox Code Playgroud)

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

G-M*_*ca' 9

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

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

  • 按照AP 的建议使用rows="${LINES:=$(tput lines)}" and ,或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"
Run Code Online (Sandbox Code Playgroud)

要使用这个:

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

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

一、美学 / 顺应传统 / Unix 哲学

Unix 的哲学是“ Do One Thing and Do It Well”。例如,如果程序要以某种方式显示数据(如寻呼机那样),那么它不应该调用产生数据的机制。这就是管道的用途。

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

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

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

二、时间问题

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

三、命令历史

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

    #!/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"
    
    Run Code Online (Sandbox Code Playgroud)

    然后你可以输入

    ps ax | mypager
    
    Run Code Online (Sandbox Code Playgroud)

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

    !:1 > myfile
    
    Run Code Online (Sandbox Code Playgroud)

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

  2. 或者假设您决定要运行ps uax下一个。如果你已经打字ps ax | mypager,你可以做

    mypager "ps ax"
    
    Run Code Online (Sandbox Code Playgroud)

    同样,使用mypager "ps ax",它仍然可行,但可以说更难。

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

四、复杂命令/引用

  1. echo {1..10000}显然只是一个示例命令; ps ax并没有好多少。如果你想要做的事情只是一个一点更逼真,像ps ax | grep oracle?如果你输入

    !:0 u!:*
    
    Run Code Online (Sandbox Code Playgroud)

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

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

    mypager ps ax | grep oracle
    
    Run Code Online (Sandbox Code Playgroud)

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

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

    命令1   " arg 1 " |   命令2   ' arg 2 ' $' arg 3 '

    这里的参数包含空格,制表符,凌乱的组合 $|\<>*;&[]()`,,甚至'"。像这样的命令可能很难直接正确地输入到 shell 中。现在想象一下必须引用它才能将它作为参数传递给mypager.


Sté*_*las 5

这就是 选项-F的用途less,尽管您也需要使用该-X选项,否则它会将文本打印到具有该选项的终端上的备用屏幕(这意味着退出后将无法随时使用该选项less)。这在未来可能会改变,因为目前有一个增强请求,要求当文本适合一个屏幕上时暗示 -X -F(303),并且RedHat 系统显然自 2008 年以来已经为此提供了补丁(尽管它还没有发布到上游(截至 2017 年 9 月 14 日,我刚刚向 bug-less@gnu.org 发送了一封关于此事的邮件))。

所以:

cmd | less -RXF
Run Code Online (Sandbox Code Playgroud)

如果输出太长时您仍然想使用备用屏幕,那么您需要花点心思(在没有上述 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;' -- "$@"
}
Run Code Online (Sandbox Code Playgroud)

用作:

cmd | page
Run Code Online (Sandbox Code Playgroud)

或者

page < file
page -S < file...
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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