8

I have Hugh file with following details :

define host{
        use                     generic-host            ; Name of host template to use
        host_name               ServerA_172.29.16.102
        alias                   ServerA_172.29.16.102
        address                 172.29.16.102
        check_command           check-host-alive
        max_check_attempts      3
        notification_interval   120
        notification_period     24x7
        }



define host{
        use                     generic-host            ; Name of host template to use
        host_name               ServerB_172.29.16.103
        alias                   ServerB_172.29.16.103
        address                 172.29.16.103
        check_command           check-host-alive
        max_check_attempts      3
        notification_interval   120
        notification_period     24x7
        }

What I want, Search "address 172.29.16.102" and Delete 4 line above and after 5 line .

I have tried following with sed but not work

sed '$N;$N;N;/address                 172.29.16.102/,+5d' hosts
Rahul Patil
  • 24,711
  • MUST it be sed, or are you open to other tools? – ghoti Sep 06 '13 at 18:22
  • if you are deleting both the 4 lines before AND 5 after, surely sed -n '/\s*address/p;' file will solve it? Which is the same as grep 'address' file? Or am I missing something here? What is the output meant to look like? – Drav Sloan Sep 06 '13 at 20:28
  • there any many entry in input file, I just want to remove some container means from define { to } if that address match, It should not remove any other content, that's it – Rahul Patil Sep 06 '13 at 20:30

6 Answers6

8

If each define_host section is separated by one or more newlines, this is exactly the kind of problem GNU awk's multiple line record support is meant to solve

awk -v RS= '!/172.29.16.102/{printf $0""RT}'
iruvar
  • 16,725
5

This is a perfect example of why sed is the stream editor, and will never replace the power of ex for in-place file editing.

ex -c '/address  *172.29.16.103/
?{?,/}/d
x' input

This command is a simplified form that's not as robust as it could be, but serves for illustration.

The first command finds the regex specified and moves the cursor to that line.

The second command consists of two addresses separated by a comma, upon which the delete command is run. ?{? searches backwards from the current line for an open curly brace, and /}/ searches forward from the current line for a close curly brace. Everything in between is deleted (linewise, so the beginning of the open curly brace line is deleted as well).

x saves the changes and exits. And input is of course the name of the file.

For the input you gave, this command works exactly as desired.


Now, I mentioned this could be improved considerably. We'll start with the regex. The most obvious outness here is that periods are wildcards; the regex as given could also match "172329-16 103". So the periods must be escaped with backslashes so they will only match literal periods.

Next is the whitespace. I put two spaces followed by a * (I could have used \+ but I don't know if that feature is required in POSIX), but what if there are tabs in the file? The best solution is to use [[:space:]]. (This would look much nicer with \+; if anyone finds whether that is POSIX please post a comment.)

Finally, what if the regex is not found in the file? Well, then the file will simply be opened for editing, and the "search" command will fail, and an error message will be printed and the rest of the given commands won't be executed--you'll be left in the ex editor so you can make changes by hand. However, if you want to automate the edits in a script, you probably want the editor to exit if no changes need to be made. The answer is to use the global command, and also to use the -s flag to suppress any output from ex:

ex -sc 'g/address[[:space:]][[:space:]]*172\.29\.16\.103/ ?{?,/}/d
x' input

This isn't quite equivalent to the earlier command; if there is more than one curly brace block with a matching line, the global command here will delete them all. That's probably what you want anyway.

If you want to only delete the first match, while exiting without changing the file if there are no matches at all, you can use the x command as part of the argument to the g command (to exit the file after the first delete command is performed) and throw in a q! command at the bottom in case the g command doesn't execute for lack of any matching lines.

ex -sc 'g/address[[:space:]][[:space:]]*172\.29\.16\.103/ ?{?,/}/d | x
q!' input

To be honest these commands make the process look much more complicated than it is; the robustness comes at the expense of extreme clarity and readability of the code. It's a trade-off.

I recommend editing a few files interactively with ex to get a bit of a feel for it. That way you can see what you're doing. Such an editing session in ex to do this fix interactively looks something like this:

$ ex input
"input" 23L, 843C
Entering Ex mode.  Type "visual" to go to Normal mode.
:/103
        host_name               ServerB_172.29.16.103
:?{?,/}/d                             # This deletes the current block

:$p                                   # Print and move to last line

:-5,.p                                # Print some more lines to check result
        notification_interval   120
        notification_period     24x7
        }



:?}?+,.d                              # Trim whitespace
        }
:x                                    # Save and exit
$ 

The POSIX specifications for ex provide further reading.

Wildcard
  • 36,499
  • 1
    Actually piping the commands to ex through the idiom printf '%s\n' 'command 1' 'command 2' | ex filename also avoids the problem of "what if the regex is not found in the file" and it's the cleaner solution that I would now recommend over some of the complicated bits in this answer. – Wildcard Sep 21 '21 at 05:24
5

When ever I see this type of question my gut tells me this is a job for grep. However grep's ability to inverse (-v) the results when using the before & after switches (-B .. & -A ..) doesn't allow for this.

However this clever approach of calling grep 2 times does it much cleaner than any of the awk or sed solutions that I've seen to date.

$ grep -v "$(grep -B 4 -A 5 'address 172.29.16.102' <file>)" <file>

Example

Here's some sample data.

$ cat sample.txt
define host{
        use                     generic-host            ; Name of host template to use
        host_name               ServerA_172.29.16.102
        alias                   ServerA_172.29.16.102
        address                 172.29.16.102
        check_command           check-host-alive
        max_check_attempts      3
        notification_interval   120
        notification_period     24x7
        }

line1b
line2b
line3b
line4b
address 172.29.16.102
line5a
line4a
line3a
line2a
line1a

define host{
        use                     generic-host            ; Name of host template to use
        host_name               ServerB_172.29.16.103
        alias                   ServerB_172.29.16.103
        address                 172.29.16.103
        check_command           check-host-alive
        max_check_attempts      3
        notification_interval   120
        notification_period     24x7
        }

Now when we run our command:

$ grep -v "$(grep -B 4 -A 5 'address 172.29.16.102' sample.txt)" sample.txt
define host{
        use                     generic-host            ; Name of host template to use
        host_name               ServerA_172.29.16.102
        alias                   ServerA_172.29.16.102
        address                 172.29.16.102
        check_command           check-host-alive
        max_check_attempts      3
        notification_interval   120
        notification_period     24x7
        }


define host{
        use                     generic-host            ; Name of host template to use
        host_name               ServerB_172.29.16.103
        alias                   ServerB_172.29.16.103
        address                 172.29.16.103
        check_command           check-host-alive
        max_check_attempts      3
        notification_interval   120
        notification_period     24x7
        }
slm
  • 369,824
  • This works fine for the OP; however, in the general case, there's a problem when the matching lines contain regex reserved characters. – Andrew Hows Oct 22 '18 at 00:59
  • This also does not work in the general case where the lines to be removed, those before and after the matched line, are not unique in the file and the others copies should not be removed. – carandraug Nov 20 '19 at 16:30
2

Rather than using strict numbers of lines with sed or grep, my preference would be to write something that does basic interpretation of the file format itself, and evaluates the contents of each record. Consider the following using awk:

#!/usr/bin/awk -f

function doit(blob) {
  if (blob !~ /address[[:space:]]+172\.29\.16\.102/) {
    print blob;
  } 
}

# Find a new record...
/^define host/ {
  doit(blob);
  blob="";
}

# Don't start each record with a blank line...
length(blob) { blob=blob "\n"; }

# Collect our data...    
{ blob=blob $0; }

END {
  doit(blob);
}

The idea here is that we'll walk through the file, and each time we see define host we'll start keeping adding each line to the variable blob. When a new record starts, or when we come to the end of the file, we process that variable using the doit() function.

This works in both GNU and classic awk.

ghoti
  • 6,602
0

With sed you can do:

sed -ne'/^[^ ]/!{H;$!d;}' <in >out \
    -e 'x;//{/\n *address  *172\.29\.16\.103/!p;}'

...which will buffer each block separately in Hold space as they are read in and only print those which don't contain a match for your pattern.

mikeserv
  • 58,310
0

What about this?

egrep -v '^([[:space:]]+(use|host_name|alias|check_command|max_check_attempts|notification_interval|notification_period)[[:space:]]+|^define host{|^[[:space:]]+})'

It removes the particular lines that are not needed, if we suppose what there is no similar lines in the file what we should not remove.

slm
  • 369,824
Ray
  • 869