LinuxQuestions.org

LinuxQuestions.org (/questions/)
-   Programming (https://www.linuxquestions.org/questions/programming-9/)
-   -   Insert line on match only once with sed? (https://www.linuxquestions.org/questions/programming-9/insert-line-on-match-only-once-with-sed-657764/)

lhouk 07-23-2008 03:05 PM

Insert line on match only once with sed?
 
I thought this would be fairly straightforward, but it's got me stumped. The following sed command will find every blank line and insert the new line "aaaa" after each one:
Code:

sed '/^$/i\
aaaa'

But can I modify it to insert the new line only once, after just the first blank line, no matter how many blank lines are in my file? Thanks in advance to all who respond.

Mr. C. 07-23-2008 06:05 PM

Sed can be a pain to work with.

Does this work for you:

Code:

perl -e '$/=undef; $_=<>; s/\n\n/\naaaa\n/; print $_'  datafile

ghostdog74 07-23-2008 08:17 PM

Code:

awk 'NF==0{print;print "insert line";next}1' file

pixellany 07-23-2008 10:58 PM

I don't see a simple way in SED. (Perhaps Mr. C and ghostdog74 did not either.....;))

SED provides a mechanism for specifying which occurrence of a pattern to act on, but it works line by line.

Note that "i" will insert your new text BEFORE the line matching the pattern. To insert after, you want "a" (append).

Best SED (and AWK) tutorial here:
http://www.grymoire.com/Unix/Sed.html#

syg00 07-23-2008 11:17 PM

Just goes to prove - use the (best) tool for the job. Hammers don't make good screw-drivers.

pixellany 07-24-2008 12:05 AM

Quote:

Originally Posted by syg00 (Post 3224445)
Hammers don't make good screw-drivers.

Well, now.....that depends on the screw, doesn't it.......;). In my motorcycle repair days, we had a saying: "Get a bigger hammer". With what we were dealing with, that was the right answer surprisingly often.

Bash scripting is like having the world biggest collection of hammers---all sizes. We all have our favorite that we grab first.

Kenhelm 07-24-2008 07:31 AM

This inserts the newline "aaaa" only after the first blank line.
Code:

sed '1,/^$/ {/^$/a\
aaaa
}'


pixellany 07-24-2008 08:09 AM

Neat....

I didn't know that the range syntax could be nested like that

lhouk 07-24-2008 09:33 AM

Quote:

Originally Posted by Kenhelm (Post 3224868)
This inserts the newline "aaaa" only after the first blank line.
Code:

sed '1,/^$/ {/^$/a\
aaaa
}'


That's exactly what I was looking for! Thank you!

And pixellany, you are quite right:

Quote:

Originally Posted by pixellany (Post 3224433)
...Note that "i" will insert your new text BEFORE the line matching the pattern. To insert after, you want "a" (append)...

I wrote "after" in my original post, when I meant to say "before". :) But Kenhelm's code works just as well with "i" as with "a".

Also, thanks to Mr. C and ghostdog74 for their responses. I appreciate the help.

Mr. C. 07-24-2008 12:07 PM

The range isn't exactly nested; the command is

[2addr] function-list

in this case the [2addr] is the range line 1 to the first blank line. The function list looks like:

{
func1
func2
...
}

and this list contains only one function, which itself looks like:

[1addr]a

where [1addr] specifies any lines matching /^$/.

I used to love working out all sorts of problems with sed, but now no longer have the patience for it.
Nice job Kenhelm.

ps. I'm still awaiting someone to solve the sed palindrome checker I've challenge sed masters with several times. :-)

pixellany 07-24-2008 12:17 PM

Yes, I was sloppy when said range. What is "nested" here is the <<address--action>> functionality of SED. Range is a special case of address.

Nesting:

<<address---
<<address--action>>
>>

Can this nesting go deeper? For example:
<<range---
<<subrange---
<<address---action>>
>>
>>

Where is the Palindrome problem posted? (Somewhere I proposed a thread on scripting puzzles--should we revive that idea?)

Mr. C. 07-24-2008 01:11 PM

I've not posted the palindrome problem officially, but have mentioned it several times. Basically, the challenge is to make a sed script that recognizes any palindrome and outputs Yes, if it is, and No if it is not.

The range is in the definition of a 2addr (which means 0, 1, or 2 addresses), and addresses take a couple of forms, such as line number, or a pattern. Example 2addr's are 4, or 3,4, or 5,/somepat/, or /pat2/,$, etc.

Can sed "nest"; sure. As long as you live within its grammer, you can do what you're asking about:

Code:

$ cat in
Section AAA
        var1 = abba
        var2 = baab
        var3 = abab
EndSection

Section BBB
        var1 = nisse
        var2 = lasse
        var3 = salle

EndSection

$ cat sed.script2
1,$ {
=
/Section/ {
p
/End/ {
a\
This is the end
}
}
}

$ sed -nf sed.script2 in
1
Section AAA
2
3
4
5
EndSection
This is the end
6
7
Section BBB
8
9
10
11
12
EndSection
This is the end


pixellany 07-24-2008 02:42 PM

Just ran some more tests....

Not only can you nest address--action pairs, but the subaddresses can overlap.

eg:
sed '1,6{1,3<do something>;2,5<do something else>;4,6<do more stuff>}' file

If each action is a change, then they happen in order---ie lines 1 thru 3 get changed, and then the changes for 2 thru 5 get applied, etc.

But, if--eg-- the action for 2,5 is delete, then the next statement acts on what WAS lines 4,6.

WOW!!! The opportunities to write obfuscated SED code just went way up.......;)

Mr. C. 07-24-2008 02:50 PM

Exactly. Just think of 2addr or 1addr as asking "does the current line number or pattern (space) match the address(es) specified in 2addr or 1addr? ... If so, do stuff".

Kenhelm 07-25-2008 03:07 AM

An answer to Mr. C.'s sed palindrome checker problem:-
Code:

#!/bin/bash
# If $1 is a palindrome output 'Yes'; otherwise output 'No'
echo $1 | sed 's/.*/&\n&\nNo\nYes/' |  # Make 4 lines: $1/$1/No/Yes
sed '2 {/\n/!G;s/\(.\)\(.*\n\)/&\2\1/;//D;s/.// }' | # 'rev' line 2
sed '$!N; /^\(.*\)\n\1$/!P; D' |                    # Emulate 'uniq'
sed -n '3p'                                        # Output line 3

The method:-
Set up 4 lines,
$1
$1
No
Yes

Emulate 'rev' only on line 2 (e.g. "abcde" becomes "edcba")

Emulate 'uniq': if $1 is a palindrome line 2 is deleted so "Yes" moves up from line 4 to replace "No" on line 3.

Output line 3.

The emulations of 'rev' and 'uniq' are from
'Useful One-line Scripts For sed' Compiled by Eric Pement
http://sed.sourceforge.net/sed1line.txt

Mr. C. 07-25-2008 01:08 PM

Hmmm, not working here.
Code:

$ echo "I am abba abba am I" | bash ./pal.sh 
Yes

$ echo "I am abba bba am I" | bash ./pal.sh 
Yes

$ echo "I am abba bb am I" | bash ./pal.sh 
Yes

$ echo "I am abb" | bash ./pal.sh         
Yes

$ echo "No workie" | bash ./pal.sh
Yes

It won't work with older sed's I'm afraid. On NetBSD:

Code:

$ echo "I am abba abba am I" | ./pal.sh 
sed: 1: "2 {/\n/!G;s/\(.\)\(.*\n ...": bad flag in substitute command: '}'

Regardless, that's multiple sed's. It has to be accomplished with one sed script (and hence invocation).

pixellany 07-25-2008 01:59 PM

Is there a prize for solving this puzzle?

How many people are known to have solved it?

When you say "one sed script", I read it to mean that the word "sed" can only appear once. For example, this would be legal:

sed -e 'stuff' -e 'morestuff' ....etc.

Mr. C. 07-25-2008 03:27 PM

Quote:

Originally Posted by pixellany (Post 3226318)
Is there a prize for solving this puzzle?

Knowledge!

Ok, maybe I'll send $5 US. PM/email me the answers, and I'll post solutions if you want to allow everyone to get a chance to solve.

Quote:

Originally Posted by pixellany (Post 3226318)
How many people are known to have solved it?

Nobody thus far has taken the challenge. I solved it years ago (along with all sorts of torturous sed scripts, using sed as a basic dual-register state machine).
Quote:

Originally Posted by pixellany (Post 3226318)
When you say "one sed script", I read it to mean that the word "sed" can only appear once. For example, this would be legal:

sed -e 'stuff' -e 'morestuff' ....etc.

Sure, sed -e A -e B is really the same as :

Code:

$ sed -f sedfile
$ cat sedfile:
A
B

so I'd call this transmutable.

Kenhelm 07-26-2008 01:59 AM

Quote:

Originally Posted by Mr. C.
Hmmm, not working here.
Code:

$ echo "No workie" | bash ./pal.sh
Yes

This is because the script is meant to be used with input from a command-line parameter e.g.
Code:

$ bash ./pal.sh "No workie"
No

If no parameter is given the script behaves as though it has been given a null string to consider and returns "Yes" (a null string is treated as a trivial case of a palindrome).
To convert the script to use piped input just remove
echo $1 |
from the first line of the script code and then using GNU sed version 4.1.4 it works with all those examples of piped input where you gave it as not working.

Quote:

Originally Posted by Mr. C.
It won't work with older sed's I'm afraid
Is it part of the challenge that the script has to work with every version of sed?

Mr. C. 07-26-2008 02:13 AM

I saw and knew that! Code that requires an argument should error out when there is no argument present. Since sed is a filter, a sed script should also act in like fashion. The challenge is "make a sed script". What you have is a shell script that calls sed ... several times. But still nice!

Since I didn't indicate which platform, it should work on any platform. Obviously this isn't reasonable if you don't have multiple environments in which to test. So we'll just call portability a bonus.

mcv 07-27-2008 01:21 AM

Simple and efficacious.

Quote:

a="Assim a aluna anula a missa"
echo "$a"|sed -e 's/\s//g' -e ':loop; s/^\(.\)\(.*\)\1$/\2/gi; t loop' -e 's/..\+/No/g' -e 's/^$\|^.$/Yes/g'


Mr. C. 07-27-2008 11:39 AM

Very nice.

No need for your /g flag on the substitution in the loop.

mcv 07-27-2008 04:01 PM

Really. Its necessary only in the first command of the sed. But because it dont affect the result, I forgot remove it.



_

was6guy 01-13-2009 02:52 PM

Need help with the same topic...
 
Im trying to search a text file for a specific string, if the string is present, i'd like to insert a newline below the string with a value.

This works fine:


Code:

#!/bin/sh

sed '/START NEW MAPPING RULES/ a\
'TEST 1.conf >> 1.conf.bak



This inserts TEST into the file below the search criteria. But when I need to supply the real world string, sed blows up. This example works via cmd line:



Code:


echo 1 | sed 's/1/proxy            \/*      http:\/\/server-tstb\.com\.com:12345\/*        server-tstb\.com\.com/g'

But when trying the same below sed fails in the script.

#!/bin/sh

sed '/START NEW MAPPING RULES/ a\
'proxy            \/*      http:\/\/server-tstb\.com\.com:12345\/*        server-tstb\.com\.com 1.conf >> 1.conf.bak


Mr. C. 01-13-2009 08:28 PM

To generally replace TEST with some arbitrary string in your shell script, you'll need to escape the metacharacters for both sed's consumption, and the shell as well. This get's tricky very quickly. Any time you can eliminate one quoting layer the better. So you might find perl's qr// operator easier to use.

How / where is the TEST string being assigned?

was6guy 01-13-2009 10:05 PM

The string im searching for is in a file that grows, but there's only one occurrence of the string in the file. Once I find the string, I need to write something along the lines of this right below the string.


proxy /* http://server-tstb.com.com:12345/* server-tstb.com.com

Mr. C. 01-14-2009 12:42 AM

Ok, this isn't so bad the via the shell. Since that's the direction you've taken, let's go that way:

Code:

#!/bin/bash

# First, assign the string we'll append to a shell variable
#
append='proxy /*xhttp://server-tstb.com.com:12345/* server-tstb.com.com'

# Place the sed pattern in double quotes will allow the append variable to expand.
# But we need to escape the sed append backslash because the shell will strip it within
# double quotes
#
sed "/pattern/ a\\
$append" 1.conf


vladtz 10-08-2009 12:41 AM

Another approach to the palindrome challenge
 
All though I saw a solution in this discussion, it did niot solve the problem completely inside sed.
This solution does just that by keeping the original word in the hold space.

This is just a filter: words go in, palindromes come out.
It does not adres the Yes/No problem, but that is trivial.

#!/usr/bin/ksh

sed -n '
h
s/^/\n/
:again
s/\(.*\n\)\(.\)/\2\1/
t again
G
/^\(.*\)\n\n\1$/{
x
p
}
' "$@"

Mr. C. 10-08-2009 10:46 AM

Welcome to the party!

Good solution. To be pedantic, this is a KSH script, not a sed script. The language is KSH, which happens to call the sed command line utility with a sed script argument. :-)

vladtz 10-09-2009 03:24 AM

Yup, you are pedantic... but, aren't we all?
Just put everything between the single quotes in a file (palindromes.sed) and run sed -f palindromes.sed file...
It will then be 100% pure sed :-)

The shell in the example doesn't add anything at all (just turns the whole thing in a convenient filter form).

Mr. C. 10-09-2009 10:57 AM

Quote:

Originally Posted by vladtz (Post 3713215)
Yup, you are pedantic... but, aren't we all?
Just put everything between the single quotes in a file (palindromes.sed) and run sed -f palindromes.sed file...
It will then be 100% pure sed :-)

The shell in the example doesn't add anything at all (just turns the whole thing in a convenient filter form).

OK, I'll go further than hint.

The form I called pedantic requires 2 processes. Filters should by nature, since you don't know how they will be called, be as cheap as possible, and no impose unnecessary performance penalties. There is too easy a solution around that penalty in this case.

Filters should not require a user to specify arbitrary script files to include.

Filters should not require a user to type out the filter script on the command line.

So, the best solution, requires 1 process invocation, no specifying of a script file, or typing of a script. Use the kernels built in interpreter execution syntax instead, placing the contents below into a file that is then made executable:

Code:

#!/usr/pkg/bin/gsed -nf
h
s/^/\n/
:again
s/\(.*\n\)\(.\)/\2\1/
t again
G
/^\(.*\)\n\n\1$/{
x
p
}


vladtz 10-10-2009 05:08 AM

OK, yes I did consider doing it that way (did forget about the -n though).
I agree completely.

John Lumby 04-30-2010 02:54 PM

In case anyone else uses the posted sed script - I believe one correction is needed ---

instead of

sed '1,/^$/ {/^$/a\
aaaa
}'

it should be

sed '0,/^$/ {/^$/a\
aaaa
}'

The difference is apparent only for an input in which both the first two lines are blank.
The originally posted script will add the aaaa line twice.
The corrected one will do so only for the first blank line as specified.

John

ArthurSittler 05-06-2010 05:29 AM

sed might not be able to recognize palindromes
 
I am not totally certain about this, but I believe that sed cannot recognize arbitrarily long palindromes. Sed has the power of regular expressions, which is equivalent to deterministic finite automaton (DFA) which is also provably equivalent to a non-deterministic finite automaton (NDFA, also known as NFA). But this is not sufficient to recognize arbitrarily long palindromes. Recognizing arbitrarily long palindromes requires the power of a push-down automaton (PDA). The NFA is not equivalent to the capability of a PDA.

grail 05-06-2010 07:13 AM

Just in case another awk alternative might be considered:
Code:

awk '/^$/ && !f++ && gsub(//,"&\nnext")||1' file

MTK358 05-06-2010 09:51 AM

This thread is over 2 years old!

grail 05-06-2010 12:05 PM

oops ... sorry ... didn't look at date of original :( my bad :redface:

chuafengru 08-09-2010 04:26 PM

Variables
 
Quote:

Originally Posted by Kenhelm (Post 3224868)
This inserts the newline "aaaa" only after the first blank line.
Code:

sed '1,/^$/ {/^$/a\
aaaa
}'


Is it possible to replace aaaa with a shell variable? im using bash script.

John Lumby 08-10-2010 09:23 AM

Yes - simply use double-quotes instead of single and then explicitly quote the $ and \ chars :
e.g.

addw=foobar;echo -e "\\n\\n" | sed "0,/^\$/ {/^\$/ a \\
${addw}
}"


All times are GMT -5. The time now is 08:09 PM.