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

6

我想知道是否可以使用一个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 ...”等等。
当然,在实践中,如果我使用多个过滤器并将它们连接起来,也不会发生什么不好的事情,我只是好奇是否可能用一个过滤器解决问题。
我希望将输出分配给一个变量。

1
很好的问题,研究得非常透彻!您能指出针对此给定输入的期望输出是什么吗?另外,您能指出您的$DHCP_HOSTNAME的值吗?这样我们也可以进行测试。 - fedorqui
2
你可以使用grep -oPlookbehind和lookahead - Sundeep
echo 'xyz 123 abc' | grep -oP 'xyz \K.*(?= abc)' 可以得到 123 - Sundeep
@fedorqui:如果我没有表达清楚,真是抱歉。${DHCP_HOSTNAME}保存一个主机名,我正在查找该主机名的MAC地址,例如“client1”。 - MegaBrutal
@spasic 这就是解决方案!为什么你不把它发布为答案呢? - MegaBrutal
@MegaBrutal,我不清楚你的问题陈述和不同情况是什么... 对我而言,它看起来像你只需要了解带有回顾/前瞻的grep -oP... 如果它有效,你也可以将其作为自己的答案发布 :) - Sundeep
4个回答

2
你可以使用 Perl 一行代码匹配文件的每一行与一个适当的捕获组的正则表达式,并对每一行匹配的子字符串进行打印。
有几种方法可以使用 Perl 完成这个任务。我建议使用 `perl -ne {program}` 惯用法,它隐式地循环遍历 stdin 的每一行,并为每一行执行一次 one-liner `{program}`,将当前行作为特殊变量 `$_` 提供。 (注意:`-n` 选项不会导致 `$_` 的最终值在隐式循环的每次迭代结束时自动打印出来,这是 `-p` 选项所做的事情;也就是说,`perl -pe {program}`。)
以下是解决方案。注意,我决定使用晦涩的 `-s` 选项传递目标主机名,该选项启用在 `program` 参数后解析变量赋值规范,类似于 awk 的 `-v` 选项。 (使用 `-n` 选项无法传递普通的命令行参数,因为隐式的 `while (<>) { ... }` 循环会吞噬所有这样的文件名参数,但 `-s` 机制提供了一个很好的解决方案。请参见 Is it possible to pass command-line arguments to @ARGV when using the -n or -p options?。)这种设计避免了将 `DHCP_HOSTNAME` 变量嵌入 `program` 字符串本身的需要,这使我们可以对其进行单引号处理并节省几个(实际上是 8 个)反斜杠。
DHCP_HOSTNAME='client3';
perl -nse 'print($1) if m(^\s*host\s*$host\s*\{.*\bhardware\s*ethernet\s*(..:..:..:..:..:..));' -- -host="$DHCP_HOSTNAME" <dhcpd.cfg;
## AB:CD:EF:01:23:45

对于以下原因,我通常更喜欢使用Perl而不是sed

  • Perl提供了完整的通用编程环境,而sed则更为有限。
  • Perl拥有大量公开可用的模块存储在CPAN中,可以通过-M{module}选项轻松安装和使用。而sed则无法扩展。
  • Perl具有比sed更强大的正则表达式引擎,包括前后查找断言、回溯控制动词、内部正则表达式和替换Perl代码、更多选项和特殊转义字符、嵌入组选项等。请参见perlre
  • 尽管Perl更为复杂,但由于其双通道处理和高度优化的操作码实现,它通常比sed更快。例如,请参见http://rc3.org/2014/08/28/surprisingly-perl-outperforms-sed-and-awk/
  • 我经常发现,相应的Perl实现比sed更直观,因为sed具有一组更原始的命令来操作底层文本。

这非常好而且非常详细,实际上还起作用!尽管我仍然选择使用grep,主要是因为习惯。无论如何,您的解决方案只缺少一件事:如果DHCP选项的顺序不同(例如,“host client4 { fixed-address 192.168.1.14; filename"pxelinux.0" ; hardware ethernet C1:CA:88:FA:F4:90; } "),此Perl脚本将无法找到MAC地址。即使我很可能永远不会有不同顺序的条目,但我仍然不想做出这种假设。 - MegaBrutal
@MegaBrutal 感谢您的留言,很高兴能够帮忙。您说得对,我忘记了支持 DHCP 选项的任何顺序的目标。我做了一个小修改,应该可以解决这个问题,即在左括号和“hardware”关键字之间用.*\b替换\s*。干杯。 - bgoldst

1
我会选择使用sed进行此操作,因为您可以使用正则表达式进行行定位:
sed -e "/host  *${DHCP_HOSTNAME}/!d" -e "s/*.\(hardware [^;]*\).*/\1/g"

第一个表达式删除不匹配 ${DHCP_HOSTNAME} 的所有行(如果您的主机名中有任何正则表达式元字符,您可能需要在 shell 中进行调整,但我假设您没有)。
第二个表达式匹配硬件地址部分,并删除该行的其余部分。

由于某种原因,它对我无效:我在sed表达式中遇到了语法错误。 我试图更正它,但找不到实际的问题所在。 我的提示是,应该将某些引号更改为硬引号,因为BASH在“!d”的位置替换垃圾。 我也尝试过使用DASH,但没有成功。 - MegaBrutal
我的错 - 我刚才看到我在第二个表达式中遗漏了替换字符串 - 现在已经编辑过了。 - Toby Speight

0

由于人们也使用不同的工具来回答问题,我认为 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",  $2, $6); }' 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"; }

-1
你可以尝试使用以下表达式和 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

上述表达式将仅从dhcp配置文件中返回MAC地址。


1
这不是我要找的内容。它确实从配置文件中提取了所有MAC地址,但我想要提取我感兴趣的特定MAC地址。例如,如果我正在寻找“client2”的MAC地址,我应该只得到“23:34:45:56:67:78”-没有多余的,也没有少的。 - MegaBrutal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接