3

Let M=3 and N=4. I want to replace line M in file1 with line N in file2. I can replace with a string using sed:

sed -i '3s/.*/stringToReplace/' file1

And I can use awk to get line N from file2

awk 'NR==4' "file2"

How can I combine these two? If I try

sed -i '3s/.*/{awk 'NR==4' "file2"}/' file1

then I replace line M with literally {awk 'NR==4' "file2"}.

don_crissti
  • 82,805
Tom
  • 33

4 Answers4

3

There are several ways to do this properly so as to handle arbitrary input.
With GNU sed and a system that supports /dev/stdin:

sed -n "${n}{p;q;}" file2 | sed -e "$m{r/dev/stdin" -e 'd;p;}' file1

or, slightly shorter1:

sed $n'!d;q' file2 | sed -e $m'{r /dev/stdin' -e 'd;p;}' file1

With any sed and a shell that supports process substitution

sed '1h;1d;'$((m+1))'x' <(sed ${n}'!d;q' file2) file1

which could also be written as

sed ${n}'!d;q' file2 | sed '1h;1d;'$((m+1))'x' - file1

Basically, one sed invocation extracts line n from file2 which is then read by the other sed as 1st operand: it saves it into the hold buffer, deletes it and then reads the content of the 2nd operand, namely file1, exchanging buffers when on line m+1 (of the combined input).

With any sed that supports reading a script-file via -f from stdin one could run:

sed ${n}'!d;i\
'${m}'c\\
s/\\/&&/g
q' file2 | sed -f - file1

here, the 1st sed turns line n from file2 into a script-file like

${m}c\
line_n_content_here_with_any_backslash_escaped

which is then used by the 2nd sed to process file1 (namely replace line m with the following text...). Any backslashes present in the original text (along with any embedded newlines - but here there's just one line) have to be escaped because when using any of a\, i\ or c\ to add text

<backslash> characters in text shall be removed, and the following character shall be treated literally.

With any sed, you can use the always-popular substitute command making sure that the string interpolated into sed substitution escapes all reserved characters - in this particular case it's just one line so e.g.

line=$(sed ${m}'!d;s|[\/&]|\\&|g;q' file2)

then substitute:

sed ${m}'s/.*/'"$line"'/' file1

With huge input files you could run:

{ head -n $((m-1)); { head -n $((n-1)) >/dev/null; head -n 1; } <file2; head -n 1 >/dev/null; cat; } <file1

which does something like this:

print (m-1) lines from file1
discard (n-1) lines from file2
print n-th line from file2
discard m-th line from file1
print the remaining lines from file1

though some heads are dumb and won't comply with the standards so this won't work on all setups... but where it does, it trumps sed, awk and the likes in terms of speed.


1: with some shells you might need to disable history expansion for that ! to work...
also, $n and $m don't really need quoting here as they're supposed to be positive integers though it doesn't hurt either

don_crissti
  • 82,805
Satō Katsura
  • 13,368
  • 2
  • 31
  • 50
  • from manual ... As a GNU sed extension, the special value /dev/stdin is supported for the file name, which reads the contents of the standard input I think GNU sed will handle /dev/stdin irrespective of system details – Sundeep Oct 06 '17 at 09:42
  • also perhaps sed -n "${n}"'{p;q}' to exit as soon as line is found – Sundeep Oct 06 '17 at 09:42
  • 1
    @Sundeep Re: /dev/stdin: could be, but my manual doesn't mention that. :) Good point about {p;q;}, edited. – Satō Katsura Oct 06 '17 at 10:36
2

Try this:

$ cat f1
foo
bar
xyz
baz
temp
good
$ cat f2
1
2
3
4
5
6

$ awk -v m=3 -v n=4 'NR==FNR{if(FNR==n) s=$0; next} FNR==m{$0=s} 1' f2 f1
foo
bar
4
baz
temp
good
  • NR==FNR will be true only when first file is being processed
  • if(FNR==n) s=$0 if it is nth line, save to variable
  • next so that rest of code is not executed as long as first file is processed
  • FNR==m{$0=s} if it is mth line for second file argument, replace it
  • 1 print input record, including any modifications
  • Note the order of file input arguments

Can use if(FNR==n){s=$0;nextfile} to avoid processing lines after nth line

From GNU awk manual - thanks @iruvar

NOTE: For many years, nextfile was a common extension. In September 2012, it was accepted for inclusion into the POSIX standard. See the Austin Group website.

Sundeep
  • 12,008
1

Short sed approach:

Sample file f1:

f1 line1
f1 line2
f1 line3
f1 line4
f1 line5

Sample file f2:

ID1,value12,value13
ID1,value22,value23
ID1,value32,value33
ID2,/value42/,~value43~
ID3,value52,value53

The job:

sed '3 s/.*/'"$(sed -n '4{ s/\//\\\//g;p;}' f2)"'/;' f1

The output:

f1 line1
f1 line2
ID2,/value42/,~value43~
f1 line4
f1 line5
  • 3
    won't work for all kinds of data in f2.. for ex: if 4th line was /4/ – Sundeep Oct 06 '17 at 09:46
  • @RomanPerekhrest that is why it is preferable to use r command as suggested in https://unix.stackexchange.com/a/396450/109046 – Sundeep Oct 06 '17 at 10:15
  • @Sundeep, you may check my update – RomanPerekhrest Oct 06 '17 at 10:49
  • @RomanPerekhrest that would still fail for other cases like 4\ or 4& etc.. but expanding on what you said earlier, solutions are acceptable according to input formats... my comments are more of a note point which you could add to the answer... – Sundeep Oct 06 '17 at 10:58
0

Don't mix awk with sed. You can use a complete awk solution

file1:

 file1 1
 file1 2
 file1 3
 file1 4

file2:

 file2 1
 file2 2
 file2 3
 file2 4


 awk -v m=3 -v n=4 'NR == FNR { filea[FNR]=$0 } FNR != NR { fileb[FNR]=$0 } END { for (i=1;i<=FNR;i++) { if ( i == m ) { print fileb[n] } else { print filea[i] } } } ' file1 file2

Broken down:

 NR == FNR { 
            filea[FNR]=$0 
           }
 FNR != NR { 
            fileb[FNR]=$0 
           }
 END { 
       for (i=1;i<=FNR;i++) { 
                               if ( i == m ) { 
                                         print fileb[n] 
                                             } 
                               else { 
                                          print filea[i] 
                                    } 
                            } 
      } 

We compare NR and FNR to determine records in file1 and file2 (when NR=FNR, we know that we are in the first file) We set up array filea with the records in file1 and fileb with the records in file2. We then loop through all the records in the array filea printing the contents only when the passed parameter m is not equal to 3. If it is, we instead print the subscript of array fileb determined by the passed parameter n