3

Assume that I have the data of two lines with the same length

abcdb#lae#blabl#a
abc~bola~xblabl~a

I need to remove the # in the first line (there can be one or many # in the first line), and also the character at the same location in the next line, to make the data become

abcdblaeblabla
abc~bla~blabla

I have tried sed '/#/{n;s/~//g}' but it removes more characters than I want.

1
  • Thank you! There can be one or many #(s) in the first line. I have edited my question
    – Jason Qin
    Apr 28, 2018 at 3:50

5 Answers 5

2

awk

These methods repeat for each pair of lines (1 and 2; 3 and 4; etc), working for as many # characters as there are in the first line of each pair, and assuming that the two lines of each pair are the same length.

Compatible with GNU awk (Linux) and BSD awk (Mac).


Using substrings:

awk '{ a=$0 ; gsub(/#/,"",$0) ; print $0 ; getline ; for (n=1;n<=length(a);n++) if ( substr(a,n,1) != "#" ) printf "%s",substr($0,n,1) ; printf "%s",RS }' file.txt

The same code, reformatted for narrower screens:

awk '{
  a=$0 ;
  gsub(/#/,"",$0) ;
  print $0 ;
  getline ;
  for (n=1;n<=length(a);n++)
    if ( substr(a,n,1) != "#" )
      printf "%s",substr($0,n,1) ;
  printf "%s",RS
  }' file.txt
  • a=$0
    Save a copy of the first line.
  • gsub(/#/,"",$0) ; print $0
    Delete all # from the first line (not from the copy), then print the modified first line.
  • getline
    Go to the next line.
  • for (n=1;n<=length(a);n++)
    Step through each character of the first-line copy.
    • if ( substr(a,n,1) != "#" )
      If this single-character substring is not #,…
      • printf "%s",substr($0,n,1)
        …then print the character from the corresponding position in the second line.
  • printf "%s",RS
    End the second line with a newline character.

Using arrays:

awk '{ c=d="" ; elements=split($0,a,"") ; getline ; split($0,b,"") ; for (n=1;n<=elements;n++) if (a[n]!="#") { c = c a[n] ; d = d b[n] } ; print c ; print d }' file.txt

Reformatted for narrower screens:

awk '{
  c=d="" ;
  elements=split($0,a,"") ;
  getline ;
  split($0,b,"") ;
  for (n=1;n<=elements;n++)
    if (a[n]!="#")
      { c = c a[n] ; d = d b[n] } ;
  print c ;
  print d
  }' file.txt
  • c=d=""
    Initialize two blank strings. These will become modified versions of the two lines of input. This step is necessary if there are more than two lines of input.
  • elements=split($0,a,"")
    Convert the first line of input into an array, with one character per array element. Store the number of array elements as the variable elements.
  • getline
    Go to the next line.
  • split($0,b,"")
    Convert the second line of input into an array, with one character per array element.
  • for (n=1;n<=elements;n++)
    Step through each element of the first-line array.
    • if (a[n]!="#")
      If this single-character array element is not #,…
      • { c = c a[n] ; d = d b[n] }
        …then, for each of the two lines, retain the character from position n.
  • print c ; print d
    Print the new versions of the two lines.

Caution: The Mac (BSD) version of awk does not automatically handle array elements in numerical order. This initially gave me surprising results.

The order in which a ‘for (indx in array)’ loop traverses an array is undefined in POSIX awk and varies among implementations. gawk lets you control the order by assigning special predefined values to PROCINFO["sorted_in"].

The GNU Awk User’s Guide

The elements are still numbered 1,2,3,... at the time of creation with split, as in GNU awk, but BSD awk does not necessarily see them in that order when using for (n in array). Thus, you'll get scrambled gibberish.

To get around this, you can store an array's length (number of elements) when you create the array – eg, elements=split($0,a,"") – and then iterate through the elements by using for (n=1;n<=elements;n++), as I've done here.


Example input (file.txt):

abcdb#lae#blabl#a
abc~bola~xblabl~a
#alpha#beta#gamma#delta#epsilon#
abcdefghijklmnopqrstuvwxyzabcdef

Example output:

abcdblaeblabla
abc~bla~blabla
alphabetagammadeltaepsilon
bcdefhijkmnopqstuvwyzabcde
2

You can do it with sed the following way. Place two markers at the beginning of the two lines, after bringing both into the pattern space.

Then start moving them to the right a character at a time. During this movement notice what lies to the immediate right of the markers and take action accordingly.

Stop when the marker hits the end of pattern space. Now take away the markers as their job is done and what remains is what u want. Note the marker is \n

 sed -Ee '
   /#/N;/\n/!b
   s/\n/&&/;s/^/\n/
   :a
       /\n#(.*\n.*\n)./{
          s//\n\1/;ba
       }
      s/\n(.)(.*\n.*)\n(.)/\1\n\2\3\n/
   /\n$/!ba
   s/\n//;s///2
'    input

Using Perl it is tackled along these lines :

 perl -pe  ' 
     next unless /#/;

     my($n,$p) = (scalar <>);

     while ( /#/g ) {
        pos($n) = pos() - 1 - $p++;
        $n =~ s/\G.//;
     }

     y/#//d;s/\z/$n/;
'      input_file 

Working:

1. Skip lines that donot have hash char.
 2. Save the next line in $n and init. $p counter which keeps track of the number of hash chars erased till now.
3.  Monitor the position of the hash char in a while loop and using info generate the position of the char to be deleted in next line.
4.  Erase it using the \G metachar in s///
5.  In the final step remove the hash chars from present line and append the next line to it.

Another method, this time using arrays is shown :

perl -aF'' -ne '
    print,next unless /#/;
    print,last if eof;

    my @I = grep { $F[$_] ne "#" } 0 .. $#F;
    my @N = split //, <>;

    print @F[@I], @N[@I];
'    input_file

Working:

1. Invoke Perl to split each line on a per character basis and have it stored in the array @F anew for every line read.
2.  Record the array indices for which the array element is a non hash character.
3.  Readin the next line, split it on a per character basis and store in array @N.
4. Now its a matter of selecting the indices we stored in @I and fetch those from arrays @F and @N.

Method of regexes:

perl -pe '
   $_ .= <> unless eof;

    s/\G.(.*\n.{@{[+pos]}})./$1/ while /(?=#.*\n.)/g;
'        input_file

Description :

° append the next line to current so long as it isn't the last line.

° Record the positions of the hash characters in the first line by means of while loop.

° Then remove the hash character in the original line and the character at the corresponding position in the next line.

° After we are out of the while loop the - p option will auto print the $_ to stdout.

Method with plain string operations :

perl -pe '
   last if eof;
   my $n = <>;
   while ( (my $p = index($_,"#")) > -1 ) {
      substr($_, $p, 1) = "" for $_, $n;
   }
   $_ .= $n;
'       input_file

This involves the use of index builtin to check the position of hash and then use that in the substr builtin two times... on the first nd next lines.

0

This is fairly easy in awk.  When you see a #, determine where in the line it is.  Then, for that line and all following lines, cut that character position out of the line.

awk '
    /#/ { pound=index($0, "#") }
        {
                if (pound)
                        print substr($0, 1, pound-1) substr($0, pound+1)
                else
                        print
        }
    '
1
  • 1
    It only allows to remove the first #, right? Sorry I have updated my question and there can be multiple # in the first line.
    – Jason Qin
    Apr 28, 2018 at 4:01
0
awk '{gsub(/#/,"")sub(/bola~x/,"bla~")sub(/~a$/,"a")}1' file

output
abcdblaeblabla
abc~bla~blabla
1
  • If I have done something wrong please tell me what it is! Apr 28, 2018 at 19:36
0

With gnu awk with the use of gensub

awk '
/#/{
  a=$0
  b=length()
  getline
  $0=a RS$0
  while($0!=a){
    a=$0
    $0=gensub("([^#]*)#(.{"b--"}).","\\1\\2",1)}
}1' infile

Explain :

/#/ : for each line with #

a=$0 : save line in a

b=length() : get the length in b

getline : get the next line

$0=a RS$0 : add the previous line stored in a at the beginning of the buffer $0 followed by RS the record separator

Now $0 contains 2 lines

while($0!=a) : while the line stored in a is different from the buffer $0

a=$0 : get the buffer $0 in a

$0=gensub("([^#]*)#(.{"b--"}).","\\1\\2",1) : remove the first # in $0 and the corresponding char in the second line

In the same time decrement (b--) the length of the first line by 1 because 1 # was remove

1 : when there is no more # in the first line print $0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .