5

I have a table somewhere in a big log file which looks like this example:

----------------------------
CARTESIAN COORDINATES (A.U.)
----------------------------
  NO LB      ZA    FRAG    MASS        X           Y           Z
   0 C     6.0000    0    12.011         -8.817666638854597         -4.911814574090662         58.264165798697491
   1 C     6.0000    0    12.011         -7.879568488830738         -4.388761616508626         55.950914108733443
   2 C     6.0000    0    12.011         -7.790669273242299         -4.339145245237274         60.527363919786708
   3 C     6.0000    0    12.011         -7.070247938157430         -3.937287748509576         62.694740665963295
   4 C     6.0000    0    12.011         -7.244178391763230         -4.034368638160922         53.748929835486599
   5 H     1.0000    0     1.008         -6.427462410780078         -3.581016558829315         64.562423911622218
   6 H     1.0000    0     1.008         -6.674286700050606         -3.718319003596096         51.850593400164620

--------------------------------
INTERNAL COORDINATES (ANGSTROEM)
--------------------------------

I want to tell awk to find the CARTESIAN COORDINATES (A.U.) then find NO LB then starts reading the second variable in each line until it reaches the blank space before -----.

So, I will read all the (elements Carbon (C) Oxygen (O) Hydrogen (H)) C's H's and ... then I get how many C's H's.

I have and I get to make a variable like C5H2 in this case, it may end up be anything like C3OH4, any ideas?

awk '
/CARTESIAN COORDINATES (A.U.)/ {fcart=1}
fcart &&
/  NO LB/ {scart=1}


/---------------------------/{exit}
' OFS="\t" "$FILENAME"

4 Answers 4

6

Use this awk:

awk '/CARTESIAN COORDINATES \(A.U.\)/{a=1;next} a==1&&/NO LB/{b=1;next} $0==""{exit}
a==1&&b==1{c[$2]++} END{for(i in c){printf "%s%s", i,c[i]}}' file
  • /CARTESIAN COORDINATES \(A.U.\)/{a=1;next}: This block searches for CARTESIAN COORDINATES (A.U.) and then sets the variable a to 1, next means to jump to the next line and start the processing again with that line.
  • a==1&&/NO LB/{b=1;next} checks if a is 1 and if the second string NO LB is found somewhere in the line. It sets the variable b and then loads the next line.
  • $0==""{exit}: Then if the line is empty exit the processing (it jumps to the END{} block).
  • a==1&&b==1{c[$2]++}: If both matches are found (a and b equal 1) increment an array called c with index $2 (field 2). This will count the occurences of each value in the second field.
  • END{...}: This will run when the file processing is done (the array is filled).
    • for(i in c) run trough each element in the array...
    • printf "%s%s", i,c[i]: ... and print the index and the value.

The output (with your example file):

C5H2
9
  • 1
    state machine FTW! Sep 29, 2015 at 20:11
  • 2
    @A.B. ^^ Today i'm thinking in awk: awk 'BEGIN{stand_up("very early")} {work("hard");} END{print "need sleep"; goto("bed"); exit}' me
    – chaos
    Sep 29, 2015 at 20:12
  • 1
    @chaos awk 'BEGIN{stop_using_awk("anti_awk_campaign")}END {print "no_more_awk"}' -> Output: FATAL ERROR !!! ;)
    – H. Freeze
    Sep 29, 2015 at 20:20
  • just to make sure it works if I have a third element there as well right like O, right? Sep 29, 2015 at 20:21
  • 1
    @RaymondGhaffarianShirazi Yes, just put the whole awk command in brackets: var="$(awk ...)"
    – chaos
    Sep 29, 2015 at 20:45
4

Yet another awk version:

awk '/NO.*[[:blank:]]LB/,/INTERNAL COORDINATES/ { 
        if($1~/[0-9]/){count[$2]++;}} 
      END {for(i in count){printf "%s%s",i,count[i]}print ""} ' file 

This is sort of a mix between Serg's answer and Chaos's. It will only run between lines matching NO.*[[:blank:]]LB and INTERNAL COORDINATES. The count array only counts on lines whose first field is a number.


If your file is exactly as you show it where successive blocks of data are separated by an empty line, you can use Perl's "paragraph mode" which treats paragraphs as lines:

perl -00ne 'next unless /CARTESIAN COORDINATES \(A\.U\.\)/; 
            $count{$_}++ for (/\s+\d+\s+(\w+)\s/g); 
            print "$_$count{$_}" for keys(%count)' file 

Explanation

  • -00: turn on paragraph mode;
  • next unless /CARTESIAN COORDINATES \(A\.U\.\)/; skip this paragraph if it doesn't match CARTESIAN COORDINATES (A.U.);
  • $count{$_}++ for (/\n\s+\d+\s+(\w+)\s/g) : the regular expression looks for one or more whitespace characters (\s+), followed by one or more digits (\d+), one or more whitespace characters again and then one or more word characters (\w+) followed by a whitespace character. This should identify all elements. %count is a hash, an associative array. It has keys and each key is associated with a value. The $count{$_}++ for ... will save each of the matches of the regex above as a key in that hash and increment its value by one each time it is found. The result is a hash that stores the elements and the number of times each was found.
  • print "$_$count{$_}" for keys(%count) : for each of the elements (the keys of the hash %count), print the element and the number of times it was found.

Run on your example file, this returns:

$ perl -00ne 'next unless /CARTESIAN COORDINATES \(A\.U\.\)/; 
            $count{$_}++ for (/\s+\d+\s+(\w+)\s/g); 
            print "$_$count{$_}" for keys(%count)' file 
C5H2$

That lacks the final newline, however, so you can add it with:

$ perl -00ne 'next unless /CARTESIAN COORDINATES \(A\.U\.\)/; 
                $count{$_}++ for (/\s+\d+\s+(\w+)\s/g); 
                print "$_$count{$_}" for keys(%count); print "\n"' file 
C5H2
2

Here's a somewhat simpler code:

awk '/NO.*[[:blank:]]LB/,/INTERNAL COORDINATES/ { if ( $2 == "C")  counterC++; if ($2 == "H") counterH++  } END {print "C"counterC"H"counterH} ' coordinates.txt

Sample output:

$ awk '/NO.*[[:blank:]]LB/,/INTERNAL COORDINATES/ { if ( $2 == "C")  counterC++; if ($2 == "H") counterH++  } END {print "C"c>
C5H2
4
  • the big issue was not looking for a specific C or H just looking to the second element and see how many times it's repeated in the same form it can be C2O3H5 or C3SiH3O Sep 29, 2015 at 20:23
  • @RaymondGhaffarianShirazi I see. Let me fix that then Sep 29, 2015 at 20:27
  • @RaymondGhaffarianShirazi will it be fine if the code just outputs number of each element ? Because making up the complete formula probably will be too complex of a code Sep 29, 2015 at 20:33
  • it's ok chaos code seems to work fine, thx Sep 29, 2015 at 20:42
2

chaos's answer works very well to accomplish what you want. Here's a simpler alternative just in case,

awk 'BEGIN{}
$2 ~ /^C$/ { countC++; } $2 ~ /^H$/ { countH++ }
END { print "C",countC,"H",countH; }' OFS="" file

Gives the output C5H2.

4
  • the big issue was not looking for a specific C or H just looking to the second element and see how many times it's repeated in the same form it can be C2O3H5 or C3SiH3O Sep 29, 2015 at 20:24
  • Note that there's no reason to initialize variables to 0 in the BEGIN block. You can just use them directly: awk '$2 ~ /^C$/ { countC++; } $2 ~ /^H$/ { countH++ }END { print "C",countC,"H",countH; }' OFS="" file
    – terdon
    Sep 29, 2015 at 23:21
  • @terdon Thanks for pointing that out, i have updated my answer accordingly too.
    – H. Freeze
    Sep 30, 2015 at 12:35
  • @RaymondGhaffarianShirazi My bad, check out chaos's answer, it works very well to that purpose
    – H. Freeze
    Sep 30, 2015 at 12:39

You must log in to answer this question.

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