【问题标题】:How to grep/sed/awk for a range of output starting with a whitespace character如何grep / sed / awk以空格字符开头的一系列输出
【发布时间】:2016-10-27 01:56:41
【问题描述】:

我有一个看起来像这样的文件:

# cat $file
...
ip access-list extended DOG-IN
 permit icmp 10.10.10.1 0.0.0.7 any
 permit tcp 10.11.10.1 0.0.0.7 eq www 443 10.12.10.0 0.0.0.63
 deny   ip any any log
ip access-list extended CAT-IN
 permit icmp 10.13.10.0 0.0.0.255 any
 permit ip 10.14.10.0 0.0.0.255 host 10.15.10.10
 permit tcp 10.16.10.0 0.0.0.255 host 10.17.10.10 eq smtp
...

我希望能够按名称搜索(使用脚本)以获得独立访问列表的“部分”输出。我希望输出看起来像这样:

# grep -i dog $file | sed <options??>

ip access-list extended DOG-IN
 permit icmp 10.10.10.1 0.0.0.7 any
 permit tcp 10.11.10.1 0.0.0.7 eq www 443 10.12.10.0 0.0.0.63
 deny   ip any any log

...不再输出不适用的非缩进行。

我尝试了以下方法:

grep -A 10 DOG $file | sed -n '/^[[:space:]]\{1\}/p'

...这只给了我 DOG 之后的 10 行,它们以一个空格开头(包括不适用于搜索到的访问列表的行)。

sed -n '/DOG/,/^[[:space:]]\{1\}/p' $file

...这给了我包含 DOG 的行,以及以单个空格开头的下一行。 (需要所有访问列表的适用行...)

我想要包含 DOG 的行,以及 DOG 之后以单个空格开头的所有行,直到下一个未缩进的行。内容中的变量太多,无法依赖前导空格以外的任何模式(结尾并不总是拒绝,等等...)。

【问题讨论】:

    标签: regex bash awk sed grep


    【解决方案1】:

    使用 GNU sed (Linux)

    name='dog'  # case-INsensitive name of section to extract
    sed -n "/$name/I,/^[^[:space:]]/ { /$name/I {p;d}; /^[^[:space:]]/q; p }" file
    

    要使匹配区分大小写,请删除上面出现/I 之后的I

    • -n 禁止默认输出,因此必须在脚本内使用 p 等函数显式请求输出。
    • 注意在sed 脚本周围使用 引号("..."),以便允许引用shell 变量$name:双引号确保在将脚本交给sed 之前扩展shell 变量引用(sed 本身无权访问shell 变量)。
      • Caveat:这种技术很棘手,因为 (a) 您必须使用 shell 转义来转义 shell 元字符,以便传递给 sed,例如 $\$,以及 (b) shell-variable 值不得包含可能破坏 sed 脚本的 sed 元字符;对于在 sed 脚本中使用的 shell 变量值的通用转义,请参阅我的 this answer,或使用我的 awk-based answer
    • 下一个 部分(/^[^[:space:]]/ - 即不以空格开头的下一行);因为sed 范围总是包含,挑战是选择性地删除范围的最后一行,如果它是 next 部分的开始 - 请注意,这将如果感兴趣的部分是文件中的最后一个部分,则不是这种情况。
      请注意,{ ... } 中的命令仅针对范围内的每一行执行。
    • /$name/I {p;d}; 无条件打印范围的第一行:d 删除该行(已打印)并开始下一个循环(继续下一个输入行)。
    • /^[^[:space:]]/q 匹配范围中的最后一行,如果它是 next 部分的第一行,则完全退出处理 (q),不打印该行。
    • p 然后只到达部分内部线并打印它们。

    注意:

    • 假设标题行可以通过不以空白字符开头来标识,并且任何其他行都是非标题行 - 如果需要更复杂的匹配,请参阅 my awk-based answer
    • 此解决方案有一个小缺点,即必须复制范围正则表达式,尽管您可以使用 shell 变量来缓解这种情况。

    FreeBSD/macOS sed几乎可以做到这一点,只是它缺少不区分大小写的选项I

    name='DOG'  # case-SENSITIVE name of section to extract
    sed -n -e "/$name/,/^[^[:space:]]/ { /$name/ {p;d;}; /^[^[:space:]]/q; p; }" file
    

    请注意,FreeBSD/OSX sed 通常有更严格的语法要求,例如命令后的; 即使后面跟着}

    如果您确实需要不区分大小写,请参阅my awk-based answer

    【讨论】:

    • +1,谢谢 - 很棒的解释 - 帮助我根据需要修改它(不打印标题 + 在第一场比赛后不要退出但继续其他比赛):sed -n "/ $name/I,/^[^[:space:]]/ { /$name/I d; /^[^[:space:]]/d; p }" 文件
    【解决方案2】:
    awk -vfound=0 '
    /DOG/{
        found = !found;
        print;
        next
    }
    
    /^[[:space:]]/{
        if (found) {
            print;
            next
        }
    }
    
    { found = !found }
    '
    

    您可以用任何 ERE 代替 /DOG/,例如 /(DOG)|(CAT)/,脚本的其余部分将完成这项工作。当然,如果你愿意,你可以把它浓缩。

    请注意,仅仅因为一行以空格开头,并不意味着只有一个空格。 /^[[:space:]]{1}/ 将匹配前导空格,即使在像

    这样的字符串中
                          nonspace
    

    表示它等同于/^[[:space:]]/。如果您的格式非常严格以至于必须始终只有一个空格,请改用/^[[:space:]][^[:space:]]/。不会匹配上面带有“非空格”的行。

    【讨论】:

      【解决方案3】:

      我添加了第二个答案,因为 mklement0 指出了我的逻辑缺陷。

      在 Perl 中这是一种非常简单的方法:

      perl -ne ' /^\w+/ &amp;&amp; {$p=0}; /DOG/ &amp;&amp; {$p=1}; $p &amp;&amp; {print}'

      示例:

      cat /tmp/file  | perl -ne ' /^\w+/ && {$p=0}; /DOG/ && {$p=1}; $p && {print}'
      ip access-list extended DOG-IN
       permit icmp 10.10.10.1 0.0.0.7 any
       permit tcp 10.11.10.1 0.0.0.7 eq www 443 10.12.10.0 0.0.0.63
       deny   ip any any log
      
      cat /tmp/file  | perl -ne ' /^\w+/ && {$p=0}; /CAT/ && {$p=1}; $p && {print}'
      ip access-list extended CAT-IN
       permit icmp 10.13.10.0 0.0.0.255 any
       permit ip 10.14.10.0 0.0.0.255 host 10.15.10.10
       permit tcp 10.16.10.0 0.0.0.255 host 10.17.10.10 eq smtp
      

      解释:

      如果该行以 [a-z0-9_] 开头,则设置 $p false

      如果该行包含 PATTERN 在这种情况下 DOG 设置 $p true

      如果 $p 打印为真

      【讨论】:

      • +1;做得很好;在awk 中它甚至会更短:awk '/^[[:alnum:]_]/ {p=0} /DOG/ {p=1} p'。 (在 POSIX awk 中,您不能使用 \w,但您可以在 GNU 中使用 awk。)。 Quibble:你并不需要+
      • 感谢 awk 替代方案,我喜欢惯用的 awk!
      【解决方案4】:

      @mklement0 将我已经难以理解的 sed 压缩到此:

      sed '/^ip/!{H;$!d};x; /DOG/I!d'
      

      将累积的多行组交换到模式缓冲区中进行处理——主要逻辑(此处为/DOG/I!d)对整个组进行操作。

      /^ip/! 通过缺少第一行标记来识别连续行并累积它们,因此x 仅在累积整个组时运行。

      某些极端情况不适用于此处:

      第一个x 在开始时交换一个幻像空组。如果在普通处理过程中没有删除,添加 1d 可以解决此问题。

      最后一个x 也交换out文件的最后一行。这通常只是最后一组的最后一行,已经由 H 累积,但如果某些命令可能会产生单行组,则需要在最后提供一个假的组(例如 echo "header phantom" | sed '/^header/!{H;$!d};x' realdata.txt -{ showgroups; echo header phantom; } | sed '/^header/!{H;$!d};x'

      【讨论】:

      • 另外,这不能简化为sed '/firstline/! {H;$!d;}; x,因此,在手头的情况下,sed '/^ip/! {H;$!d;}; x; /DOG/!d' file?我不清楚1d 在您的解决方案中做了什么;删除它似乎没有什么区别。请注意,如果最后一行是 header 行,则两种解决方案都会忽略它。
      • x 交换之前积累的组,第一行没有。你说得对,这里没有区别。你是对的,这不适用于混合的多行和单行组,但对于像这样的命令输出后处理器来说,这几乎不是问题。
      • +1 是最短的解决方案,一般来说是一个方便的sed 成语,尽管它肯定不容易理解。请注意,对于不区分大小写的匹配,您需要 GNU sed 及其 I 匹配选项:/dog/I!d。显然,这个解决方案依赖于所有以字符串 ip 开头的标题行 - 给定示例输入数据,这是一个公平的假设。
      • 感谢您澄清1d - 一般情况下将其放在那里是有意义的。考虑到这一点:sed '/firstline/! {H;$!d;}; x; 1d' 是否等同于您的解决方案(因此稍微简单一些),还是我遗漏了什么?
      • 谢谢!我重建了答案以纳入改进并直接解决您的问题,非常感谢您的帮助。
      【解决方案5】:

      一个更短的、符合POSIX 的awk 解决方案,它是@Tiago's excellent Perl-based answer 的通用和优化转换。

      sed 解决方案相比,这些答案的一个优点是它们使用 literal substring 匹配而不是正则表达式,这允许传入任意搜索字符串,而无需担心转义.也就是说,如果您确实想要正则表达式匹配,请使用~ 运算符而不是index() 函数;例如,index($0, name) 将变为 $0 ~ name。然后,您必须确保为 name 传递的值不包含意在被视为文字的意外正则表达式元字符是故意制作的正则表达式。

      name='DOG' # Case-sensitive name to search for.
      
      awk -v name="$name" '/^[^[:space:]]/ {if (p) exit; if (index($0,name)) {p=1}}  p' file
      
      • 选项-v name="$name"定义awk变量name基于shell变量$name的值(awk没有直接访问 shell 变量)。
      • 变量 p 用作一个标志来指示当前行是否应该被打印,即它是否是感兴趣的部分的一部分;只要p 未初始化,它就会在布尔上下文中被视为0 (false)。
      • 模式 /^[^[:space:]]/ 仅匹配标题行(以非空白字符开头的行),并且仅为它们处理相关的动作 ({...}):
        • if (p) exit 完全退出处理,如果 p 已经设置,因为这意味着已经到达 next 部分。立即退出的好处是不必处理文件的其余部分。
        • if (index($0, name)) 在手头的标题行中查找感兴趣的名称作为 文字子字符串,如果找到(在这种情况下 index() returns the 1-based position at which the substring was found, which is interpreted astruein a Boolean context), sets flagpto1( {p=1}`)。
      • p 仅打印当前行,如果 p1,否则不执行任何操作。也就是说,一旦找到感兴趣的部分标题,就会打印它和后续行(直到下一个部分或输入文件的末尾)。
        请注意,这是一个仅模式命令的示例:仅指定了一个模式(条件),没有关联的操作 ({...}),在这种情况下,默认操作是打印当前线,如果模式评估为真。 (该技术在常用速记 1 中用于简单地无条件打印当前记录。)

      如果需要不区分大小写

      name='dog' # Case-INsensitive name to search for.
      
      awk -v name="$name" \
        '/^[^[:space:]]/ {if(p) exit; if(index(tolower($0),tolower(name))) {p=1}}  p' file
      

      警告:macOS 附带的基于 BSD 的 awk(自 10.12.1 起仍适用)不支持 UTF-8。 : 不区分大小写的匹配不适用于ü等非ASCII字母。

      GNU awk 替代,使用特殊的 IGNORECASE 变量:

      awk -v name="$name" -v IGNORECASE=1 \
        '/^[^[:space:]]/ {if(p) exit; if(index($0,name)) {p=1}}  p' file
      

      另一个符合 POSIX 的awk 解决方案:

      name='dog' # Case-insensitive name of section to extract.
      
      awk -v name="$name" '
       index(tolower($0),tolower(name)) {inBlock=1; print; next} # 1st section line found.
       inBlock && !/^[[:space:]]/       {exit}             # Exit at start of next section.
       inBlock                                             # Print 2nd, 3rd, ... section line.
       ' file
      

      注意:

      • next 跳过剩余的模式-动作对并继续下一行。
      • /^[[:space:]]/ 匹配以 至少一个 空白字符开头的行。正如@Chrono Kitsune 在他的回答中解释的那样,如果您想匹配以 exactly one 空格字符开头的行,请使用/^[[:space:]][^[:space:]]/。另请注意,尽管它的名称,字符类 [:space:] 匹配任何形式的空白,而不仅仅是空格 - 请参阅 man isspace
      • 无需初始化标志变量inBlock,因为它在数字/布尔上下文中默认为0
      • 如果您有 GNU awk,您可以通过将 IGNORECASE 变量设置为非零值 (-v IGNORECASE=1) 并简单地在内部使用 index($0, name) 来更轻松地实现不区分大小写的匹配程序。

      一个 GNU awk 解决方案,如果,您可以假设所有节标题行都以 'ip' 开头(以便以这种方式将输入分成多个部分,而不是寻找前导空格):

      awk -v RS='(^|\n)ip' -F'\n' -v name="$name" -v IGNORECASE=1 '
        index($1, name) { sub(/\n$/, ""); print "ip" $0; exit }
        ' file
      
      • -v RS='(^|\n)ip' 按位于字符串 'ip' 的行起始实例之间的行将输入分成记录。
      • -F'\n' 然后将每条记录按行分成字段($1,...)。
      • index($1, name) 在当前记录的 first 行查找名称 - 不区分大小写,感谢 -v IGNORECASE=1
      • sub(/\n$/, "") 删除任何尾随 \n,这可能源于感兴趣的部分是输入文件中的最后一个
      • print "ip" $0 打印匹配的记录,包括整个感兴趣的部分 - 因为,但是记录不包括 分隔符'ip',它是前置的。

      【讨论】:

        【解决方案6】:

        我能想到的最简单的方法是:sed '/DOG/, /^ip/ !d' | sed '$d'

        cat file | sed '/DOG/, /^ip/ !d' | sed '$d'
        ip access-list extended DOG-IN
         permit icmp 10.10.10.1 0.0.0.7 any
         permit tcp 10.11.10.1 0.0.0.7 eq www 443 10.12.10.0 0.0.0.63
         deny   ip any any log
        

        解释:

        • 第一个 sed 命令从包含DOG 的行打印到以ip 开头的下一行
        • 第二个sed命令删除最后一行(即以ip开头的行)

        【讨论】:

        • 盲目删除最后一行的问题是,如果感兴趣的块是文件中的 last 块,它将无法按预期工作:因为最后一个块赢了' 不会被 next 块中的一行终止(因为根据定义没有),你最终会删除一个你不应该删除的行。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-12-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多