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

Jam*_*ven 14 pipe shell-script text-processing fifo

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

line A
line B
@@inline-code-start
line X
line Y
line Z
@@inline-code-end
line C
line D
Run Code Online (Sandbox Code Playgroud)

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

输出应如下所示:

line A
line B
     1 line X
     2 line Y
     3 line Z
line C
line D
Run Code Online (Sandbox Code Playgroud)

文件中可能有几个这样的部分需要转换。

更新 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
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

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

结束更新 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
Run Code Online (Sandbox Code Playgroud)

我用它运行:

cat test-inline-codify | ./inline-codify
Run Code Online (Sandbox Code Playgroud)

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

line A
line B
     1  line X
     1  line Y
     1  line Z
line C
line D
Run Code Online (Sandbox Code Playgroud)

我的下一次尝试是使用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
Run Code Online (Sandbox Code Playgroud)

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

line A
line B
line C
line D
     1  line 1
     2  line 2
     3  line 3
Run Code Online (Sandbox Code Playgroud)

可能有一些缓存正在进行。

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

mik*_*erv 7

我同意你的看法——这可能一个普遍的问题。不过,一些常见的实用程序有一些处理它的设施。


nl

nl中,例如,分离输入到逻辑页作为-d由两个字符elimited部定界符。一行中出现的三个单独表示标题的开始,两个是正文,一个是页脚。它用输出中的空行替换输入中发现的任何这些 - 这是它唯一打印的空行

我修改了您的示例以包含另一部分并将其放入./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
Run Code Online (Sandbox Code Playgroud)

然后我运行了以下命令:

sed 's/^@@.*start$/@@@@@@/
     s/^@@.*end$/@@/'  <infile |
nl -d@@ -ha -bn -w1
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

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

以上印...

                                        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
Run Code Online (Sandbox Code Playgroud)

GNU sed

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

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

以上sed在模式空间中收集输入,直到它有足够的时间成功传递替换Test 并停止b回溯到:label。当它这样做时,它e执行nl输入表示<<为其所有其余模式空间的此处文档。

工作流程是这样的:

  1. /^@@.*start$/!b
    • 如果^整条生产线$!没有/匹配/上面的图案,那么它是b在我们只用一系列的线,这与模式开始工作,所以从这个角度-办牧场出来的剧本和autoprinted。
  2. s//nl <<\\@@/
    • s//字段/代表sed尝试匹配的最后一个地址- 因此此命令替换整@@.*startnl <<\\@@
  3. :l;N
    • :命令定义了一个分支标签——这里我设置了一个名为:label 的标签。的N电话分机命令追加输入到图案空间中的下一行后跟一个\newline字符。这是\nsed模式空间中获得ewline的少数几种方法之一- \newline 字符sed对于已经这样做了一段时间的人来说是一个确定的分隔符。
  4. s/\(\n@@\)[^\n]*end$/\1/
    • 这种s///替换只能在遇到开始后并且只有在第一次出现结束行时才能成功。它只会作用于一个模式空间,其中最后一个\newline 紧随其后是@@.*end标记$模式空间的最后。当它起作用时,它用\1第一个\(\), 或替换整个匹配的字符串\n@@
  5. Tl
    • TEST命令转移到一标签(如果有的话),如果自上次输入线被拉入模式空间没有发生成功的替换(像我一样W / N。这意味着每次将\newline 附加到与您的结束分隔符不匹配的模式空间时,Test 命令都会失败并分支回到:label,这导致sed拉入Next 行并循环直到成功。
  6. e

    • 当结束匹配的替换成功并且脚本没有分支返回失败的Test 时,sede执行一个l看起来像这样的命令:

      nl <<\\@@\nline X\nline Y\nline Z\n@@$
      
      Run Code Online (Sandbox Code Playgroud)

您可以通过将最后一行编辑为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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

sedprint 每一行,直到遇到开始标记 - 当它q完全适合输入时。该-unbuffered开关是必要的GNUsed因为它能够缓冲相当贪婪,否则,而是-根据规范-其他POSIX sedS的关系没有任何特殊考虑工作-只要<infile是一个普通文件。

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

1
line M
2
line N
3
line O
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

这些只是示例 - 任何事情都可以在 test 或 do 块中完成,但第一个实用程序不能消耗太多输入。

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


ter*_*don 2

我能想到的最简单的解决方法是不使用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
Run Code Online (Sandbox Code Playgroud)

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

$ foo.sh < file
line A
line B
    1 line X
    2 line Y
    3 line Z
line C
line D
Run Code Online (Sandbox Code Playgroud)