I'm interested whether I could use a single grep command for the following situation.
I have a dhcpd.conf file in which DHCP hosts are defined. Given the hostname, I need to find its MAC address in the dhcpd.conf file. I need to use it to disable its PXE boot config, but that's not part of this question.
The file's syntax is uniform, but I still want to make it a little fool-proof. Here is how the hosts are defined:
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"; }
We assume that all configurations take only one line, even though the dhcpd.conf syntax would allow to break options to several lines. We assume that the order of options may differ, however.
I came up with the following grep command:
grep -o "^[^#]*host.*${DHCP_HOSTNAME}.*hardware ethernet.*..:..:..:..:..:..;" /etc/dhcp/dhcpd-hosts.conf
It is supposed to ignore lines those are commented, allow arbitrary whitespace between tokens, and match until the end of the MAC address. When I run it, I get lines like this:
host client1 { hardware ethernet 12:23:34:56:78:89;
This is great! But the point is that I only need a MAC address, without the preceding trash. Now I know that using another grep, or cut, or awk to cut only the MAC address from this output would be trivial. But I wonder, is there a way to use a single grep command to get the end result, without having to pipe this output into another filter? Obviously I can't leave out the beginning of the pattern, because I want to get a specific hostname, thus matching for "..:..:..:..:..:..
" would give me all the MAC addresses.
Once again, I want a single command (not necessarily grep) which cuts out only the proper MAC address from the file. Thus I am not interested in any solutions those say "grep ... | grep ..." or "grep ... | cut ...", etc..
Of course, in practice, nothing bad happens if I use multiple filters and pipe them, I am just curious whether it is possible to solve with one filter.
I would assign the output to a variable.
Grep: Print only the words of the line that matched the regular expression, one per line. We used the following parameters on our command: -h, –no-filename : Suppress the prefixing of file names on output. This is the default when there is only one file (or only standard input) to search.
The easiest of the two commands is to use grep's -w option. This will find only lines that contain your target word as a complete word. Run the command "grep -w hub" against your target file and you will only see lines that contain the word "hub" as a complete word.
Options Description -c : This prints only a count of the lines that match a pattern -h : Display the matched lines, but do not display the filenames. -i : Ignores, case for matching -l : Displays list of a filenames only. -n : Display the matched lines and their line numbers.
For BSD or GNU grep you can use -B num to set how many lines before the match and -A num for the number of lines after the match. If you want the same number of lines before and after you can use -C num . This will show 3 lines before and 3 lines after.
You can use a Perl one-liner to match each line of the file against a single regex with an appropriate capture group, and for each line that matches you can print the submatch.
There are several ways to use Perl for this task. I suggest going with the perl -ne {program}
idiom, which implicitly loops over the lines of stdin and executes the one-liner {program}
once for each line, with the current line made available as the $_
special variable. (Note: The -n
option does not cause the final value of $_
to be automatically printed at the end of each iteration of the implicit loop, which is what the -p
option would do; that is, perl -pe {program}
.)
Below is the solution. Note that I decided to pass the target hostname using the obscure -s
option, which enables parsing of variable assignment specifications after the {program}
argument, similar to awk's -v
option. (It is not possible to pass normal command-line arguments with the -n
option because the implicit while (<>) { ... }
loop gobbles up all such arguments for file names, but the -s
mechanism provides an excellent solution. See Is it possible to pass command-line arguments to @ARGV when using the -n or -p options?.) This design prevents the need to embed the $DHCP_HOSTNAME
variable in the {program}
string itself, which allows us to single-quote it and save a few (actually 8) backslashes.
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
I often prefer Perl to sed
for the following reasons:
sed
is more limited.-M{module}
option. sed
is not extensible.sed
due to its two-pass process and highly optimized opcode implementation. See http://rc3.org/2014/08/28/surprisingly-perl-outperforms-sed-and-awk/ for example.sed
, since sed
has a more primitive set of commands for manipulating the underlying text.I'd choose sed for this, because you can use a regexp for line addressing:
sed -e "/host *${DHCP_HOSTNAME}/!d" -e "s/*.\(hardware [^;]*\).*/\1/g"
The first expression deletes all lines not matching ${DHCP_HOSTNAME}
(you might want to massage this in the shell if you might have any regexp metacharacters in your hostnames, but I'll assume you don't).
The second expression matches the hardware address portion, and deletes the rest of the line.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With