Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Print only a part of a match with grep

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.

like image 689
MegaBrutal Avatar asked Aug 30 '16 15:08

MegaBrutal


People also ask

How do I print only a grep matched string?

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.

How do you grep words only?

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.

Which option of the grep command prints only a count of the lines that match a pattern?

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.

How do you grep 3 lines after a match?

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.


2 Answers

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:

  • Perl provides a complete general-purpose programming environment, whereas sed is more limited.
  • Perl has an enormous repository of publicly available modules on CPAN which can easily be installed and then used with the -M{module} option. sed is not extensible.
  • Perl has a much more powerful regular expression engine than sed, with lookaround assertions, backtracking control verbs, within-regex and replacement Perl code, many more options and special escapes, embedded group options, and more. See perlre.
  • Counter-intuitively, despite its greater sophistication, Perl is often much faster than 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.
  • I often find that the equivalent Perl implementation is more intuitive than that of sed, since sed has a more primitive set of commands for manipulating the underlying text.
like image 114
bgoldst Avatar answered Oct 16 '22 04:10

bgoldst


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.

like image 45
Toby Speight Avatar answered Oct 16 '22 03:10

Toby Speight