过滤或管道传输文件的某些部分

过滤或管道传输文件的某些部分

我有一个输入文件,其中一些部分用开始和结束标记划分,例如:

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D

我想对此文件应用转换,以便通过某些命令(nl例如 )过滤 X、Y、Z 行,但其余行保持不变。请注意,nl(数字线)跨线累积状态,因此它不是应用于每条线 X、Y、Z 的静态变换。 (编辑:有人指出nl可以在不需要累积状态的模式下工作,但我只是nl作为一个例子来简化问题。实际上,该命令是一个更复杂的自定义脚本。我真正寻找的是一个通用的解决方案来解决将标准过滤器应用于输入文件的一个子部分的问题

输出应如下所示:

line A
line B
     1 line X
     2 line Y
     3 line Z
line C
line D

文件中可能有多个需要转换的此类部分。

更新2我最初并没有指定如果有多个部分会发生什么,例如:

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D
 @@inline-code-start
line L
line M
line N
@@inline-code-end

我的期望是状态只需要在给定的部分内维护,给出:

line A
line B
     1 line X
     2 line Y
     3 line Z
line C
line D
     1 line L
     2 line M
     3 line N

但是,我认为将问题解释为要求跨部分保留状态是有效的,并且在许多情况下都很有用。

结束更新2

我的第一个想法是构建一个简单的状态机来跟踪我们所处的部分:

#!/usr/bin/bash
while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
  elif [[ $line == @@inline-code-end* ]]
  then
    active=false
  elif [[ $active = true ]]
  then
    # pipe
  echo $line | nl
  else
    # output
    echo $line
  fi
done

我用它运行:

cat test-inline-codify | ./inline-codify

这是行不通的,因为每次调用nl都是独立的,因此行号不会增加:

line A
line B
     1  line X
     1  line Y
     1  line Z
line C
line D

我的下一次尝试是使用 fifo:

#!/usr/bin/bash
mkfifo myfifo
nl < myfifo &
while read line
do
  if [[ $line == @@inline-code-start* ]]
  then
    active=true
  elif [[ $line == @@inline-code-end* ]]
  then
    active=false
  elif [[ $active = true ]]
  then
    # pipe
    echo $line > myfifo
  else
    # output
    echo $line
  fi
done
rm myfifo

这给出了正确的输出,但顺序错误:

line A
line B
line C
line D
     1  line 1
     2  line 2
     3  line 3

可能正在进行一些缓存。

我这一切都错了吗?这似乎是一个非常普遍的问题。我觉得应该有一个简单的管道来解决这个问题。

答案1

我会同意你的观点 - 可能一个普遍的问题。不过,一些常见的实用程序有一些处理它的设施。


nl

nl,例如,将输入分成逻辑页-d两个字符分隔节定界符。一行中出现 3 次单独表示一个事件的开始标题, 两个身体和一个页脚。它将输入中找到的任何这些内容替换为输出中的空行 - 这是它打印的唯一空行

我更改了您的示例以包含另一部分并将其放入./infile.所以它看起来像这样:

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D
@@start
line M
line N
line O
@@end

然后我运行了以下命令:

sed 's/^@@.*start$/@@@@@@/
     s/^@@.*end$/@@/'  <infile |
nl -d@@ -ha -bn -w1

nl可以告诉累积状态跨逻辑页面,但默认情况下不会。相反,它会根据以下方式对其输入的行进行编号风格,并由部分。所以-ha意味着数字全部标头线路和-bn手段没有身体线条- 因为它开始于身体状态。

在我了解到这一点之前,我曾经用于nl任何输入,但在意识到这nl可能会根据其默认-d限制符​​扭曲输出后\:,我学会了更加小心地使用它,并开始用于grep -nF ''未经测试的输入。但那天学到的另一个教训是,它nl可以非常有用地应用于其他方面 - 例如这个方面 - 如果你只修改它的输入一点点 - 就像我sed上面所做的那样。

输出

  line A
  line B

1       line X
2       line Y
3       line Z

  line C
  line D

1       line M
2       line N
3       line O

这里有更多关于nl- 你注意到上面除了编号行之外的所有行都是如何以空格开头的吗?当nl数字行时,它会在每个行的开头插入一定数量的字符。对于这些行,它不编号 - 即使是空白 - 它总是通过在未编号行的头部插入 ( -width count + eparator len ) * 空格来匹配缩进。-s这使您可以通过将未编号的内容与编号的内容进行比较来准确地重现未编号的内容 - 并且无需付出很大的努力。当您认为这nl会将其输入划分为逻辑部分,并且您可以-s在其编号的每一行的开头插入任意字符串时,那么处理其输出就变得非常容易:

sed 's/^@@.*start$/@@@@@@/
     s/^@@.*end/@@/; t
     s/^\(@@\)\{1,3\}$/& /' <infile |
nl -d@@ -ha -bn -s' do something with the next line!
'

上面的打印...

                                        line A
                                        line B

 1 do something with the next line!
line X
 2 do something with the next line!
line Y
 3 do something with the next line!
line Z

                                        line C
                                        line D

 1 do something with the next line!
line M
 2 do something with the next line!
line N
 3 do something with the next line!
line O

GNUsed

如果nl不是您的目标应用程序,那么 GNUsed可以e根据匹配为您执行任意 shell 命令。

sed '/^@@.*start$/!b
     s//nl <<\\@@/;:l;N
     s/\(\n@@\)[^\n]*end$/\1/
Tl;e'  <infile

上面sed收集模式空间中的输入,直到它有足够的输入来成功通过替换Test 并停止b牧场回 abel :l。当它执行时,它会使用表示为此处文档的输入来e执行其模式空间的所有其余部分。nl<<

工作流程是这样的:

  1. /^@@.*start$/!b
    • 如果^整行与上述模式不匹配$!则会从脚本中删除并自动打印 - 因此从现在开始,我们只处理以该模式开头的一系列行。//b
  2. s//nl <<\\@@/
    • s//字段/代表最后一次sed尝试匹配的地址 - 因此此命令会替换整@@.*startnl <<\\@@
  3. :l;N
    • :命令定义了一个分支标签 - 这里我设置了一个名为:label 的标签。 ext命令N将下一行输入附加到模式空间,后跟一个\newline 字符。这是\n在模式空间中获得 ewline 的仅有的几种方法之一sed- ewline 字符对于已经这样做了一段时间的 der\n来说是一个确定的分隔符。sed
  4. s/\(\n@@\)[^\n]*end$/\1/
    • s///替代只有在经过一段时间后才能成功开始遇到并且仅在第一次出现以下情况时遇到结尾线。它只会作用于最后的\newline 紧随其后的模式空间,标记模式空间的@@.*end末尾。$当它起作用时,它将用\1第一\(\), 或替换整个匹配的字符串\n@@
  5. Tl
    • est命令T分支到一个标签(如果提供)如果自上次将输入行拉入模式空间以来尚未发生成功的替换(正如我所做的那样N。这意味着每次\n将 ewline 附加到与结束定界符不匹配的模式空间时,Test 命令都会失败并分支回 abel :l,这会导致sed拉入Next 行并循环直至成功。
  6. e

    • 当结束匹配的替换成功并且脚本不会分支回失败的Test 时,sed将执行如下e命令:l

      nl <<\\@@\nline X\nline Y\nline Z\n@@$
      

您可以通过编辑最后一行来亲自查看这一点,使其看起来像Tl;l;e.

它打印:

line A
line B
     1  line X
     2  line Y
     3  line Z
line C
line D
     1  line M
     2  line N
     3  line O

while ... read

最后一种方法,也许也是最简单的方法,是使用while read,但有充分的理由。贝壳 -(最特别的是bash外壳)- 在处理大量或稳定的输入方面通常非常糟糕。这也是有道理的 - shell 的工作是逐个字符地处理输入,并调用其他可以处理更大内容的命令。

但关于其作用的重要一点是 shell一定不 read过多的输入 - 它被指定为不是缓冲输入或输出到它消耗太多或没有及时转发足够的程度,以至于它调用的命令缺少字节。所以read可以提供出色的输入测试-return有关是否还有剩余输入的信息,您应该调用下一个命令来读取它 - 但它通常不是最好的方法。

然而,这是一个如何使用的示例read 同步处理输入的其他命令:

while   IFS= read -r line        &&
case    $line in (@@*start) :;;  (*)
        printf %s\\n "$line"
        sed -un "/^@@.*start$/q;p";;
esac;do sed -un "/^@@.*end$/q;=;p" |
        paste -d: - -
done    <infile

每次迭代发生的第一件事就是read拉一条线。如果成功,则意味着循环尚未到达 EOF,因此在case匹配开始分隔do符块立即执行。否则,printf打印$lineitread并被sed调用。

sed将要p打印每一行,直到遇到开始标记 - 当它q完全适合输入时。 nbuffered开关-u对于 GNU 是必要的,sed因为否则它可以相当贪婪地缓冲,但是 - 根据规范 - 其他 POSIXsed应该无需任何特殊考虑即可工作 - 只要<infile是常规文件。

当第一个sed quits 时,shell 执行do循环块 - 它调用另一个sed打印每一行,直到遇到结尾标记。它将其输出通过管道传输到paste,因为它在各自的行上打印行号。像这样:

1
line M
2
line N
3
line O

paste然后将它们粘贴到:字符上,整个输出如下所示:

line A
line B
1:line X
2:line Y
3:line Z
line C
line D
1:line M
2:line N
3:line O

这些只是示例 - 可以在此处的 test 或 do 块中完成任何操作,但第一个实用程序不得消耗太多输入。

所有涉及的实用程序都依次读取相同的输入并打印其结果。这种事情可能很难掌握 - 因为不同的实用程序会比其他实用程序缓冲更多 - 但您通常可以依靠dd, head, 和sed来做正确的事情(不过,对于 GNU sed,您需要 cli 开关)你应该永远能够依赖read- 因为它本质上是,非常慢。这就是为什么上面的循环每个输入块只调用它一次。

答案2

一种可能性是使用 vim 文本编辑器来完成此操作。它可以通过 shell 命令传输任意部分。

一种方法是通过行号,使用:4,6!nl.此 ex 命令将在第 4-6 行(含)上运行 nl,从而在示例输入中实现您想要的效果。

另一种更具交互性的方法是使用行选择模式 (shift-V) 和箭头键或搜索来选择适当的行,然后使用:!nl。示例输入的完整命令序列可以是

/@@inline-code-start
jV/@@inline-code-end
k:!nl

这不太适合自动化(使用例如 sed 的答案更好),但对于一次性编辑来说,它非常有用,而不必求助于 20 行 shellscript。

如果您不熟悉 vi(m),您至少应该知道在这些更改之后您可以使用:wq.

答案3

我能想到的最简单的解决方法是不使用nl而是自己计算行数:

#!/usr/bin/env bash
while read line
do
    if [[ $line == @@inline-code-start* ]]
    then
        active=true
    elif [[ $line == @@inline-code-end* ]]
    then
        active=false
    elif [[ $active = true ]]
    then
        ## Count the line number
        let num++;
        printf "\t%s %s\n" "$num" "$line"
    else
        # output
        printf "%s\n" "$line"
    fi
done

然后您在该文件上运行它:

$ foo.sh < file
line A
line B
    1 line X
    2 line Y
    3 line Z
line C
line D

答案4

编辑添加了一个选项来定义用户提供的过滤器

#!/usr/bin/perl -s
use IPC::Open2;
our $p;
$p = "nl" unless $p;    ## default filter

$/ = "\@\@inline-code-end\n";
while(<>) { 
   chomp;
   s/\@\@inline-code-start\n(.*)/pipeit($1,$p)/se;
   print;
}

sub pipeit{my($text,$pipe)=@_;
  open2(my $R, my $W,$pipe) || die("can open2");
  local $/ = undef;
  print $W $text;
  close $W;
  return <$R>;
}

默认过滤器是“nl”。要更改过滤器,请使用选项“-p”和一些用户提供的命令:

codify -p="wc" file

或者

codify -p="sed -e 's@^@ ║ @; 1s@^@ ╓─\n@; \$s@\$@\n ╙─@'" file

最后一个过滤器将输出:

line A
line B
 ╓─
 ║ line X
 ║ line Y
 ║ line Z
 ╙─
line C
line D

更新1 IPC::Open2 的使用存在扩展问题:如果超出缓冲区大小,则可能会阻塞。 (在我的机器中,如果 64K 则管道缓冲区大小对应于 10_000 x“Y 行”)。

如果我们需要更大的东西(我们需要更多的 10000 条“Y 线”):

(一)安装与使用use Forks::Super 'open2';

(2) 或将函数 pipelineit 替换为:

sub pipeit{my($text,$pipe)=@_;
  open(F,">","/tmp/_$$");
  print F $text;
  close F;
  my $out = `$pipe < /tmp/_$$ `;
  unlink "/tmp/_$$";
  return $out;
}

相关内容