'sed' replace last patern and delete other

2019-05-31 13:48发布

问题:

I want to replace only the last string "delay" by "ens_delay" in my file and delete the others one before the last one:

Input file:

alpha_notify_teta=''
alpha_notify_check='YES'
text='CRDS'
delay=''
delay=''
delay=''
textfileooooop=''
alpha_enable='YES'
alpha_hostnames=''
alpha_orange='YES'
alpha_orange_interval='300'
alpha_notification_level='ALL'
expression='YES'
delay='9'
textfileooooop=''
alpha_enable='YES'
alpha_hostnames=''

Output file: (expected value)

alpha_notify_teta=''
alpha_notify_check='YES'
text='CRDS'

textfileooooop=''
alpha_enable='YES'
alpha_hostnames=''
alpha_orange='YES'
alpha_orange_interval='300'
alpha_notification_level='ALL'
expression='YES'
ens_delay='9'
textfileooooop=''
alpha_enable='YES'
alpha_hostnames=''

Here my first command but it doesn't work because it will work only if I have delay as last line.

sed -e '$,/delay/ s/delay/ens_delay/'

My second command will delete all lines contain "delay", even "ens_delay" will be deleted.

sed -i '/delay/d'

Thank you

回答1:

This might work for you (GNU sed):

sed '/^delay=/,$!b;/^delay=/!H;//{x;s/^[^\n]*\n\?//;/./p;x;h};$!d;x;s/^/ens_/' file

Lines before the first line beginning delay= should be printed as normal. Otherwise, a line beginning delay= is stored in the hold space and subsequent lines that do not begin delay= are appended to it. Should the hold space already contain such lines, the first line is deleted and the remaining lines printed before the hold space is replaced by the current line. At the end of the file, the first line of the hold space is amended to prepend the string ens_ and then the whole of the hold space is printed.



回答2:

You cannot do this kind of thing with sed. There is no way in sed to "look forward" and tell if there are more matches to the pattern. You can kind of look back, but that won't be sufficient to solve this problem.

This perl script will solve it:

#!/usr/bin/perl
use strict;
use warnings;
my ($seek, $replacement, $last, @new) = (shift, shift, 0);
open(my $fh, shift) or die $!;
my @l = <$fh>;
close($fh) or die $!;
foreach (reverse @l){
    if(/$seek/){
        if ($last++ == 0){
            s/$seek/$replacement/;
        } else {
            next;
        }
    }
    unshift(@new, $_);
}
print join "", @new;

Call like:

./script delay= ens_delay= inputfile

I chose to entirely eliminate lines which you intended to delete rather than collapse them in to a single blank line. If that is really required then it's a bit more complicated: the first such line in any consecutive set (or rather the last such) must be pushed on to the output list and you have to track whether this has just been done so you know whether to push the next time, too.

You could also solve this problem with awk, python, or any number of other languages. Just not sed.



回答3:

Have this monster:

sed -e "1,$(expr $(sed -n '/^delay=/=' your_file.txt | tail -1) - 1)"'s/^delay=.*$//' \
    -e 's/^delay=/ens_delay=/' your_file.txt

Here:

  • sed -n '/^delay=/=' your_file.txt | tail -1 return the last line number of the encountered pattern (let's name it X)
  • expr is used to get the X-1 line
  • "1,X-1"'[command]' means "perform this command betwen the first and the X-1 line included (I used double quotes to let the expansion getting done)
  • 's/^delay=.*$//' the said [command]
  • -e 's/^delay=/ens_delay=/' the next expression to perform (will occur only on the last line)

Output:

alpha_notify_teta=''
alpha_notify_check='YES'
text='CRDS'



textfileooooop=''
alpha_enable='YES'
alpha_hostnames=''
alpha_hsm_backup_notification='YES'
alpha_orange='YES'
alpha_orange_interval='300'
alpha_notification_level='ALL'
expression='YES'
ens_delay='9'
textfileooooop=''
alpha_enable='YES'
alpha_hostnames=''
alpha_hsm_backup_notification='YES'

If you want to delete the lines instead of leaving them blank:

sed -e "1,$(expr $(sed -n '/^delay=/=' your_file.txt | tail -1) - 1)"'{/^delay=.*$/d}' \
    -e 's/^delay=/ens_delay=/' your_file.txt


回答4:

As was mentioned elsewhere, sed can't know which occurrence of a substring is the last one. But awk can keep track of things in arrays. For example, the following will delete all duplicate assignments, as well ask making your substitution:

awk 'BEGIN{FS=OFS="="} $1=="delay"{$1="ens_delay"} !($1 in a){o[++i]=$1} {a[$1]=$0} END{for(x=0;x<i;x++) printf "%s\n",a[o[x]]}' inputfile

Or, broken out for easier reading/comments:

BEGIN {
  FS=OFS="="       # set the field separator, to help isolate the left hand side
}

$1=="delay" {
  $1="ens_delay"   # your field substitution
}

!($1 in a) {
  o[++i]=$1        # if we haven't seen this variable, record its position
}

{
  a[$1]=$0         # record the value of the last-seen occurrence of this variable
}

END {
  for (x=0;x<i;x++)          # step through the array,
    printf "%s\n",a[o[x]]    # printing the last-seen values, in the order
}                            # their variable was first seen in the input file.

You might not care about the order of the variables. If so, the following might be simpler:

awk 'BEGIN{FS=OFS="="} $1=="delay"{$1="ens_delay"} {o[$1]=$0} END{for(i in o) printf "%s\n", o[i]}' inputfile

This simply stores the last-seen line in an array whose key is the variable name, then prints out the content of the array in an unknown order.



回答5:

Assuming I understand your specifications properly, this should do what you need. Given infile x,

$: last=$( grep -n delay x|tail -1|sed 's/:.*//' )

This grep's the file for all lines with delay and returns them with the line number prepended with a colon. The tail -1 grabs the last of those lines, ignoring all the others. sed 's/:.*//' strips the colon and the actual line content, leaving only the number (here it was 14.)

That all evaluates out to assign 14 as $last.

$: sed '/delay/ { '$last'!d; '$last' s/delay/ens_delay/; }' x
alpha_notify_teta=''
alpha_notify_check='YES'
text='CRDS'
textfileooooop=''
alpha_enable='YES'
alpha_hostnames=''
alpha_orange='YES'
alpha_orange_interval='300'
alpha_notification_level='ALL'
expression='YES'
ens_delay='9'
textfileooooop=''
alpha_enable='YES'
alpha_hostnames=''

Apologies for the ugly catenation. What this does is writes the script using the value of $last so that the result looks like this to sed:

$: sed '/delay/ { 14!d; 14 s/delay/ens_delay/; }' x

sed reads leading numbers as line selectors, so what this script of commands do -

First, sed automatically prints lines unless told not to, so by default it would just print every line. The script modifies that.

/delay/ {...} is a pattern-based record selector. It will apply the commands between the {} to all lines that match /delay/, which is why it doesn't need another grep - it handles that itself. Inside the curlies, the script does two things.

First, 14!d says (only if this line has delay, which it will) that if the line number is 14, do not (the !) delete the record. Since all the other lines with delay won't be line 14 (or whatever value of the last one the earlier command created), those will get deleted, which automatically restarts the cycle and reads the next record.

Second, if the line number is 14, then it won't delete, and so will progress to the s/delay/ens_delay/ which updates your value.

For all lines that don't match /delay/, sed just prints them as-is.