使用 grep 仅打印匹配项的一部分

Print only a part of a match with grep

我想知道是否可以在以下情况下使用单个 grep 命令。

我有一个 dhcpd.conf 文件,其中定义了 DHCP 主机。给定主机名,我需要在 dhcpd.conf 文件中找到它的 MAC 地址。我需要用它来禁用它的 PXE 启动配置,但这不是这个问题的一部分。

该文件的语法是统一的,但我还是想让它更简单一些。以下是主机的定义方式:

    host client1 { hardware ethernet 12:23:34:56:78:89; fixed-address 192.168.1.11; filename "pxelinux.0"; }
    host client2 { hardware ethernet 23:34:45:56:67:78; fixed-address 192.168.1.12; filename "pxelinux.0"; }
    host client3 { hardware ethernet AB:CD:EF:01:23:45; fixed-address 192.168.1.13; filename "pxelinux.0"; }
    host client4 { hardware ethernet C1:CA:88:FA:F4:90; fixed-address 192.168.1.14; filename "pxelinux.0"; }

我们假设所有配置只需要一行,即使 dhcpd.conf 语法允许将选项分成多行。但是,我们假设选项的顺序可能不同。

我想到了以下 grep 命令:

grep -o "^[^#]*host.*${DHCP_HOSTNAME}.*hardware ethernet.*..:..:..:..:..:..;" /etc/dhcp/dhcpd-hosts.conf

它应该忽略那些被注释的行,允许标记之间的任意空格,并且匹配直到 MAC 地址的末尾。 当我 运行 它时,我得到这样的行:

host client1 { hardware ethernet 12:23:34:56:78:89;

太棒了!但关键是我只需要一个MAC地址,不需要前面的垃圾。现在我知道使用另一个 grep、cut 或 awk 从该输出中仅剪切 MAC 地址是微不足道的。但我想知道,有没有一种方法可以使用单个 grep 命令来获得最终结果,而不必将此输出通过管道传输到另一个过滤器?显然我不能省略模式的开头,因为我想获得一个特定的主机名,因此匹配“..:..:..:..:..:..”会给我所有的 MAC 地址。

再一次,我想要一个命令(不一定是 grep),它只从文件中删除正确的 MAC 地址。因此,我对那些说 "grep ... | grep ..." 或 "grep ... | cut ..." 等的任何解决方案都不感兴趣。

当然,在实践中,如果我使用多个过滤器并通过管道传输它们,没有什么不好的事情发生,我只是好奇是否可以用一个过滤器来解决。

我会将输出分配给一个变量。

我会为此选择 sed,因为您可以使用正则表达式进行行寻址:

sed -e "/host  *${DHCP_HOSTNAME}/!d" -e "s/*.\(hardware [^;]*\).*//g"

第一个表达式删除所有不匹配 ${DHCP_HOSTNAME} 的行(如果你的主机名中可能有任何正则表达式元字符,你可能想在 shell 中修改它,但我假设你没有't).

第二个表达式匹配硬件地址部分,并删除该行的其余部分。

你可以用这个表达式试试 Grep -o:

grep -o "[0-9A-F]\{2\}:[0-9A-F]\{2\}:[0-9A-F]\{2\}:[0-9A-F]\{2\}:[0-9A-F]\{2\}:[0-9A-F]\{2\}"

输出:

12:23:34:56:78:89
23:34:45:56:67:78
AB:CD:EF:01:23:45
C1:CA:88:FA:F4:90

上面的表达式将 return 只有来自 dhcp 配置文件的 MAC 地址。

您可以使用 Perl 单行代码将文件的每一行与具有适当捕获组的单个正则表达式进行匹配,对于匹配的每一行,您可以打印子匹配项。

有多种方法可以使用 Perl 来完成此任务。我建议使用 perl -ne {program} 习惯用法,它隐式循环 stdin 的行并为每一行执行一次 {program} 一次,当前行作为 $_ 特殊可用多变的。 (注意:-n 选项 而不是 导致 $_ 的最终值在隐式循环的每次迭代结束时自动打印,这就是-p 选项可以;即 perl -pe {program}。)

下面是解决方法。请注意,我决定使用晦涩的 -s 选项传递目标主机名,这允许在 {program} 参数之后解析变量赋值规范,类似于 awk 的 -v 选项。 (无法使用 -n 选项传递正常的命令行参数,因为隐式 while (<>) { ... } 循环吞噬了文件名的所有此类参数,但 -s 机制提供了一个很好的解决方案. 参见 .) 这种设计避免了将 $DHCP_HOSTNAME 变量嵌入 {program} 字符串本身的需要,这允许我们将其单引号并节省一些(实际上是 8 个)反斜杠.

DHCP_HOSTNAME='client3';
perl -nse 'print() if m(^\s*host\s*$host\s*\{.*\bhardware\s*ethernet\s*(..:..:..:..:..:..));' -- -host="$DHCP_HOSTNAME" <dhcpd.cfg;
## AB:CD:EF:01:23:45

sed 相比,我更喜欢 Perl,原因如下:

  • Perl 提供了一个完整的通用编程环境,而 sed 则更为有限。
  • Perl 在 CPAN 上有一个巨大的公开模块库,可以很容易地安装,然后与 -M{module} 选项一起使用。 sed 不可扩展。
  • Perl 拥有比 sed 更强大的正则表达式引擎,具有环视断言、回溯控制动词、内部正则表达式和替换 Perl 代码、更多选项和特殊转义、嵌入式组选项等。参见 perlre
  • 与直觉相反,尽管 Perl 更复杂,但由于其两遍过程和高度优化的操作码实现,Perl 通常比 sed 快得多。例如,参见 http://rc3.org/2014/08/28/surprisingly-perl-outperforms-sed-and-awk/
  • 我经常发现等效的 Perl 实现比 sed 更直观,因为 sed 有一组更原始的命令来操作底层文本。

由于人们也使用不同的工具回答,我认为 awk 可能也是一个不错的选择。

$ cat so
host client1 { hardware ethernet 12:23:34:56:78:89; fixed-address 192.168.1.11; filename "pxelinux.0"; }
host client2 { hardware ethernet 23:34:45:56:67:78; fixed-address 192.168.1.12; filename "pxelinux.0"; }
#host client3 { hardware ethernet AB:CD:EF:01:23:45; fixed-address 192.168.1.13; filename "pxelinux.0"; }
host client3 { hardware ethernet AB:CD:EF:01:23:45; fixed-address 192.168.1.13; filename "pxelinux.0"; }
host client4 { hardware ethernet C1:CA:88:FA:F4:90; fixed-address 192.168.1.14; filename "pxelinux.0"; }
$ awk '/^[^#]/ && /client3/ { printf ("%s: %s\n",  , ); }' so
client3: AB:CD:EF:01:23:45;

我使用双重匹配来排除注释行,并简单地使用字段索引来打印出想要的信息。这样,移除 PXE 部分也应该很容易。例如,删除 host3 的 filename 指令可以如下完成:

$ awk '/^[^#]/ && /client3/ { gsub(/filename[^;]+;/, ""); print; }' so
host client3 { hardware ethernet AB:CD:EF:01:23:45; fixed-address 192.168.1.13;  }

指定自定义图像 (pxecustom.0):

$ awk '/^[^#]/ && /client3/ { gsub(/filename[^;]+;/, "filename \"pxecustom.0\";"); print; }' so
host client3 { hardware ethernet AB:CD:EF:01:23:45; fixed-address 192.168.1.13; filename "pxecustom.0"; }