I have a file like this with tab delimiter:

Chr1    mak   gene    120221  120946  .       +       .       ID=spa-h0003.02;Name=spa-h0003.02
Chr1    mak   mRNA    120221  120946  .       +       .       ID=spa-cap_Chr1_00M;Parent=spa-h0003.02;Name=spa-cap_Chr1_00M
Chr1    mak   exon    120221  120946  .       +       .       Parent=spa-cap_Chr1_00M
Chr1    mak   gene    18546165        18546939        .       +       .       ID=spa-h0004.02;Name=spa-h0004.02
Chr1    mak   mRNA    18546165        18546939        .       +       .       ID=spa-cap_Chr1_18;Parent=spa-h0004.02;Name=spa-cap_Chr1_18
Chr1    mak   exon    18546165        18546504        .       +       .       Parent=spa-cap_Chr1_18
Chr1    mak   exon    18546791        18546939        .       +       .       Parent=spa-cap_Chr1_18

I want to replace different strings only if the third column has "gene". But strings on the ninth column should be replaced according to information present in a second file like this one (with tabs):

spa-h0003.02  spa-cap_Chr1_00M
spa-h0004.02  spa-cap_Chr1_18

I am not sure how to do it. I was thinking something like (XX should be the info from the second file?):

cat file | awk '$3 == "gene" && $9 == "spa-" {$9 = "XX"} {print}' 

But how I can use the information from the second file? Maybe:

while read n k; do sed -i 's/$n/$k/g' file1; done < fileA
  • With the awk answers, you'll see NR == FNR which is a very common awk idiom meaning "when the line number of all lines seen equals the line number of this file" which can only be true for the first file given. – glenn jackman Oct 18 '18 at 15:57

Assuming file1 contains the text to be replaced, file2 contains the replacement text, and you can rely on the ID= to perform the lookup between both, you can use this (more popular i guess) awk script:

awk -F'\t' '
    a[$1]=$2                                   # fills the array a with the replacement text
  $3=="gene"{                                  # check only lines with 'gene'
    id=gensub("ID=([^;]*);.*","\\1",1,$9);     # extract the id string
    if(id in a)                                # if the id is part of the array a
       gsub(id,a[id])                          # replace it
  1                                            # print the line
' file2 file1
  • thanks, Oliv. Great explanation, I can understand step by step and learn from that. – Paul Oct 18 '18 at 16:09
  • @oliv: shouldn't you limit the gsub to $9? – RudiC Oct 18 '18 at 22:02
  • @RudiC You could but this has no influence on the given example. – oliv Oct 19 '18 at 6:16

An unpopular choice: Tcl. Tcl has a nice string map command that accomplishes exactly this. Unfortunately, Tcl is not really built for perl-ish one-liners.

echo '
    # read the mapping file into a list
    set fh [open "mapping" r]
    set content [read $fh]
    close $fh
    set mapping [regexp -all -inline {\S+} $content]

    # read the contents of the data file
    # and apply mapping to field 9 when field 3 is "gene"
    set fh [open "file" r]
    while {[gets $fh line] != -1} {
        set fields [split $line \t]
        if {[lindex $fields 2] eq "gene"} {
            lset fields 8 [string map $mapping [lindex $fields 8]]
        puts [join $fields \t]
    close $fh
' | tclsh

With awk, I'd write:

awk -F'\t' -v OFS='\t' '
    NR == FNR {repl[$1]= $2; next}
    $3 == "gene" {
        for (seek in repl) 
            while ((idx = index($9, seek)) > 0) 
                $9 = substr($9, 1, idx-1) repl[seek] substr($9, idx + length(seek))
' mapping file

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.