Friday, November 30, 2012

Learn AWK by Examples

Here are some AWK (rather gawk) tricks that I normally use to tackle data crunching problems. You may also want to find out how shell variables can be passed to awk

Print all users in /etc/passwd start with a, c, or d

$ awk -F: '
/^[acd]/ { 
    print $1
}' /etc/passwd

daemon
colord
avahi-autoipd
avahi
chihung

Process stdin based on patterns as range

$ echo 'BEGIN
abc
def
END
junk
junk
START
pqrst
uvwxyz
STOP' | awk '
/START/,/STOP/ { print "-->" $0 }
/BEGIN/,/END/  { print "==>" $0 }
'

==>BEGIN
==>abc
==>def
==>END
-->START
-->pqrst
-->uvwxyz
-->STOP

Print 10 random number 10<=N<20, use /dev/null as a dummy input file

$ awk -v n=10 -v start=10 -v end=20 '
BEGIN {
    srand()
    for (i=1; i<=n; ++i) {
        printf("%d\n", start+rand()*(end-start))
    }
}' /dev/null

17
13
15
19
10
11
19
13
11
19

Count by file types in current directory

$ ls -l | 
awk '
BEGIN {
    dirs=0
    files=0
    socks=0
    links=0
}
/total/ { next }
/^d/ { ++dirs }
/^-/ { ++files }
/^s/ { ++socks }
/^l/ { ++links }
END { print "dirs=" dirs, "files=" files, "socks=" socks, "links=" links }
'

dirs=3 files=10 socks=0 links=0

Count by file types in current directory (using array)

$ ls -l | 
awk '
$1 != "total" {
    c1=substr($1,1,1)
    ++s[c1]
}
END {
    printf("dirs=%d files=%d socks=%d links=%d\n",
        s["d"], s["-"], s["s"], s["l"])
}
'

dirs=3 files=10 socks=0 links=0

Calculate total size of all .gz files in /usr/share directory

$ find /usr/share -type f -name "*.gz" -ls | 
awk '
{
    s+=$7
}
END {
    printf("%.2lf MB\n", s/(1024*1024))
}
'

63.08 MB

Count files by users in /home directory

$ find /home -mount -type f -ls | 
awk '
{
    ++count[$5]
    size[$5]+=$7
} 
END {
    for ( i in count ) {
        printf("User=%s Count=%d Size=%s\n", i, count[i], size[i])
    }
}
'

User=chihung Count=16301 Size=6346658343
User=root Count=10 Size=80109

Print all users start with e, f, g with their corresponding group name. Group id to name mapping is stored in the gid2name array by processing the first file /etc/group. (Note: I present two similar ways to do the same task)

$ awk -F: '
NR==FNR {
    gid2name[$3]=$1
}
NR>FNR && /^[e-g]/ {
    print $1, gid2name[$4]
}' /etc/group /etc/passwd

games games
gnats gnats
gdm gdm
games games
gnats gnats
gdm gdm

$ awk -F: '
FILENAME=="/etc/group" {
    gid2name[$3]=$1
}
FILENAME=="/etc/passwd" && /^[e-g]/ {
    print $1, gid2name[$4]
}' /etc/group /etc/passwd

games games
gnats gnats
gdm gdm
games games
gnats gnats
gdm gdm


Count all file extensions in /usr/include directory

find /usr/include -mount -type f | 
$ awk -F/ '
{
    basename=$NF
    n=split(basename, arr, ".")
    if ( n>1 ) {
        ext=arr[n]
        ++summary[ext]
    }
}
END {
    for ( i in summary ) {
        print i, summary[i]
    }
}
'

h 4335
def 1
x 17
hpp 245
c 6
tcc 37

Multi-line record with blank line(s) as separator

$ cat ~/.mozilla/firefox/profiles.ini 
[General]
StartWithLastProfile=1

[Profile0]
Name=default
IsRelative=1
Path=5d0x3te1.default

$ awk '
BEGIN {
    FS="\n"
    RS=""
}
{
    for ( i=1; i<=NF; ++i ) {
        print "NR=" NR, "NF=" i, "Data=" $i
    }
}' ~/.mozilla/firefox/profiles.ini

NR=1 NF=1 Data=[General]
NR=1 NF=2 Data=StartWithLastProfile=1
NR=2 NF=1 Data=[Profile0]
NR=2 NF=2 Data=Name=default
NR=2 NF=3 Data=IsRelative=1
NR=2 NF=4 Data=Path=5d0x3te1.default

Print all the section headers in a .ini file with function definition to remove square brackets

$ cat ~/.mozilla/firefox/profiles.ini 
[General]
StartWithLastProfile=1

[Profile0]
Name=default
IsRelative=1
Path=5d0x3te1.default

$ awk '                                             
function rmsq(n) {
    gsub("\\[","",n)
    gsub("]","",n)
    return n
}
BEGIN {
    FS="\n"
    RS=""
}
{
    print rmsq($1)
}' ~/.mozilla/firefox/profiles.ini

General
Profile0

Labels:

Saturday, October 16, 2010

Key-Value Pair in Multi-line Records

If you have to deal with multi-line records, AWK is definitely your friend. Sometimes the records come in the form of first record is the key and the next record is the value. To make matter worse, the value record can be optional, ie it may not exist at all.

For example, the following sample data set have the above characteristics. What we need is a script to list down the values for a particular pool name. The first 3 lines are headers which can we going to ignore.

$ cat testdata.txt
media media robot robot robot side/ ret    size status
 ID type type   # slot face level  KBytes
----------------------------------------------------------------------------
One pool

A00001 HCART    TLD   0   26  -  10     101568 AVAILABLE

Two pool

B00001 HCART    NONE   -   -  -  -     - AVAILABLE
B00002 HCART    NONE   -   -  -  -     - AVAILABLE

Three pool

C00001 HCART    NONE   -   -  -  -     - AVAILABLE
C00002 HCART    NONE   -   -  -  -     - AVAILABLE
C00003 HCART    NONE   -   -  -  -     - AVAILABLE

Zero pool

Four pool

D00001 HCART2   NONE   -   -  -  -     - AVAILABLE
D00002 HCART3   NONE   -   -  -  -     - AVAILABLE
D00003 HCART3   NONE   -   -  -  -     - AVAILABLE
D00004 HCART3   NONE   -   -  -  -     - AVAILABLE

By changing the default AWK FS (field separator) and RS (record separator) to be newline and blank line repsectively, we are able to handle the above multi-line records. In order to accommodate zero 'value' in a record, we need to put in the logical in the AWK code.

$ cat pool.sh
#! /bin/sh

PATH=/usr/bin:/bin:/usr/sbin:/usr/local/bin
LD_LIBRARY_PATH=/usr/lib:/lib:/usr/local/lib


if [ $# -ne 2 ]; then
 echo "Usage: $0  \n"
 exit 1
fi
txtfile=$1
pool=$2
if [ ! -f $txtfile ]; then
 echo "Error. $txtfile does not exist\n"
 exit 2
fi


sed -n '4,$p' $txtfile | awk -v pool="$pool" '
BEGIN {
 FS="\n"
 RS=""
 poolname=sprintf("%s pool", pool)
}
$1 == poolname {
 start=1
 next
}
start==1 && $0 ~ /pool/ {
 exit
}
start==1 {
 for (i=1;i<=NF;++i ) {
         print $i
 }
 exit
}'

See script in action.

$ ./pool.sh testdata.txt One
A00001 HCART    TLD   0   26  -  10     101568 AVAILABLE

$ ./pool.sh testdata.txt Two
B00001 HCART    NONE   -   -  -  -     - AVAILABLE
B00002 HCART    NONE   -   -  -  -     - AVAILABLE

$ ./pool.sh testdata.txt Three
C00001 HCART    NONE   -   -  -  -     - AVAILABLE
C00002 HCART    NONE   -   -  -  -     - AVAILABLE
C00003 HCART    NONE   -   -  -  -     - AVAILABLE

$ ./pool.sh testdata.txt Four
D00001 HCART2   NONE   -   -  -  -     - AVAILABLE
D00002 HCART3   NONE   -   -  -  -     - AVAILABLE
D00003 HCART3   NONE   -   -  -  -     - AVAILABLE
D00004 HCART3   NONE   -   -  -  -     - AVAILABLE

$ ./pool.sh testdata.txt Unknown

$ ./pool.sh testdata.txt Zero

$ 

So what is the big deal ? This is the kind of output you will get from NetBackup /usr/openv/netbackup/bin/goodies/available_media. With this script, you can handle the available_media output with ease.

Labels: ,

Thursday, November 19, 2009

The AWK Way

Today I was given the task of converting few hundred files (743 to be exact) into CSV format. The filename is prefixed with hostname with a fix suffix and the content contains all the local user names. The task is to put them in rows with hostname in the 1st column and usernames in the 2nd column onwards. One more requirement is to exclude a few users in the output. My initial solution is very much unix shell script-based. Although this is an one-off 'throw-away' solution, it is pretty inefficient because there is a lot of process creation within a for loop. It took 1 min 39.453 sec. After some thoughts, I reckoned it is possible to do it efficiently in just AWK. With the help of some of the built-in variables like FILENAME / NR / FNR, we can process all the input files within a single AWK code. The below code works in Cygwin. The runtime for the AWK code is 2.797 sec, that's 35 times faster !
$ ls *txt
host1_root.txt  host2_root.txt  host3_root.txt  host4_root.txt

$ paste *txt
usera   usere   userm   userx
userb   userx   userx   userw
userc   userf   usern   usery
userd   userg   usero   userz
userdx  usery   userp
userdy  userh   userx
        userz   userq
        useri   userqx
        userj   userr
        userk   userz
        userl   users
        userx   usert
                usery

$ cat a.awk
#! /usr/bin/awk -f


BEGIN {
        suffix="_root.txt"
        len=length(suffix)
}
#
# print CR if first line in input file except first file
FNR==1 && NR>1 {
        printf("\n")
}
#
# print hostname
FNR==1 {
        host=substr(FILENAME, 0, length(FILENAME)-len)
        printf("%s", host)
}
#
# print users, but exclude certain users
$0 !~ /^(userx|usery|userz)$/ {
        printf(",%s", $0)
}


$ ./a.awk *.txt
host1,usera,userb,userc,userd,userdx,userdy
host2,usere,userf,userg,userh,useri,userj,userk,userl
host3,userm,usern,usero,userp,userq,userqx,userr,users,usert
host4,userw

Labels: , ,

Saturday, April 11, 2009

An Interview With One of the AWK Inventors

Read this interview with one of the AWK inventors, Brian Kernighan.

AWK is definitely one of my favourite tools in my Swiss Army Knife

Labels:

Friday, April 10, 2009

paste: too many files- limit 12, in Solaris

When you collect performance data from system, very often you would want to prepend with timestamp (HH:MM:SS). Suppose you collect similar data across a number of servers, you would want to put them together and have them imported in your favourite spreadsheet software for further analysis.

In UNIX, you can paste them together. Below I will create 20 files (host*.txt) with some random data prepended with timestamp

$ for i in `perl -e '$,=" ";print 1..20'`
do
        for j in 1 2 3 4 5 6 7 8 9
        do
                ((v=RANDOM%100))
                echo "0$j:00:00 $v"
        done > host$i.txt
done

$ paste host1.txt host2.txt host3.txt host4.txt
01:00:00 56     01:00:00 61     01:00:00 83     01:00:00 50
02:00:00 59     02:00:00 1      02:00:00 96     02:00:00 72
03:00:00 31     03:00:00 33     03:00:00 71     03:00:00 60
04:00:00 54     04:00:00 29     04:00:00 61     04:00:00 36
05:00:00 62     05:00:00 69     05:00:00 25     05:00:00 36
06:00:00 2      06:00:00 72     06:00:00 76     06:00:00 8
07:00:00 69     07:00:00 59     07:00:00 91     07:00:00 89
08:00:00 51     08:00:00 75     08:00:00 80     08:00:00 61
09:00:00 17     09:00:00 12     09:00:00 59     09:00:00 83

Looks promising. Now, I need to get rid of the redundant timestamp. With AWK, we just have to take the even field values.

$ paste host1.txt host2.txt host3.txt host4.txt | awk '
{
        printf("%s\t%d", $1, $2)
        for ( i=4 ; i<=NF ; i+=2 ) {
                printf("\t%d",$i)
        }
        printf("\n")
}'
01:00:00        56      61      83      50
02:00:00        59      1       96      72
03:00:00        31      33      71      60
04:00:00        54      29      61      36
05:00:00        62      69      25      36
06:00:00        2       72      76      8
07:00:00        69      59      91      89
08:00:00        51      75      80      61
09:00:00        17      12      59      83

So far so good. Now I want to do that for all the hosts (host*.txt). You can use Bash brace expansion to supply the arguments to paste

$ paste host{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20}.txt
paste: too many files- limit 12

Ouch! paste cannot take more than 12 files. Stuck ? What we can do is to do one file at a time within a loop. Since I want the output to be imported to spreadsheet, I will output that as comma separated values (CSV) file (all_hosts.csv)

$ tmpfile=".tmp-$$"

$ cp /dev/null all_hosts.csv

$ for i in `perl -e '$,=" ";print 1..20'`
do
        f="host$i.txt"
        paste all_hosts.csv $f > $tmpfile
        mv $tmpfile all_hosts.csv
done

Now we have all the hosts data in one file. The final step is to remove the redundant timestamp.

$ awk '
{
        printf("%s,%d", $1, $2)
        for ( i=4 ; i<=NF ; i+=2 ) {
                printf(",%d",$i)
        }
        printf("\n")
}' all_hosts.csv
01:00:00,56,61,83,50,22,6,83,10,6,88,75,61,46,24,33,8,82,90,29,90
02:00:00,59,1,96,72,80,63,5,61,42,90,7,24,78,58,5,85,35,79,0,46
03:00:00,31,33,71,60,99,41,61,92,34,84,61,46,8,1,52,10,21,82,84,69
04:00:00,54,29,61,36,7,85,69,2,26,42,56,82,17,14,93,95,45,76,3,37
05:00:00,62,69,25,36,54,42,81,8,2,94,44,10,44,28,64,68,96,22,9,45
06:00:00,2,72,76,8,96,21,85,35,89,92,93,98,31,99,67,25,77,43,73,9
07:00:00,69,59,91,89,39,72,11,45,90,9,28,15,22,3,66,64,83,46,60,40
08:00:00,51,75,80,61,22,60,61,12,37,66,24,34,92,21,63,99,27,45,40,35
09:00:00,17,12,59,83,32,44,78,91,16,89,97,52,81,52,51,59,78,14,85,49

Labels: , , ,

Tuesday, April 07, 2009

Think Big

Suppose you have some command output consists of hexadecimal that require to be converted to decimal number, likely you will use bc to work it out
$ bc
ibase=16
ABCDEF
11259375
abcdef
syntax error on line 3, teletype
0089
137
^D

So if you want to make use of it in shell script, you need to do the following. Bear in mind that bc only accept uppercase hex number.

$ echo "ibase=16; ABCDEF" | bc
11259375

$ echo "ibase=16; abcdef" | bc
syntax error on line 1, teletype

$ echo "ibase=16; abcdef" | tr '[a-z]' '[A-Z]' | bc
syntax error on line 1, teletype
1123455

$ echo "abcdef" | tr '[a-z]' '[A-Z]' | xargs -I{} echo 'ibase=16;{}' | bc
11259375

This may work fine for small data set. However, if you need to loop through thousands of lines to do hex to dec convert, it becomes a performance problem. Below I am going to show you what are the alternatives.

Traditional shell script way. You can see how slow it is when we throw it with large data set.

$ cat hex.input
drive1 0089
drive2 0a2f
drive3 1FFE
drive4 980B
drive5 0011780c

$ cat hex2dec-sh.sh
#! /bin/sh


if [ $# -ne 1 ]; then
        echo "Usage: $0 <hex-input>"
        exit 1
fi


cat $1 | while read drive hex
do
        dec=`echo $hex | tr '[a-z]' '[A-Z]' | xargs -I{} echo "ibase=16; {}" | bc`
        echo "Drive=$drive, Hex=$hex, Dec=$dec"
done

$ time ./hex2dec-sh.sh hex.input
Drive=drive1, Hex=0089, Dec=137
Drive=drive2, Hex=0a2f, Dec=2607
Drive=drive3, Hex=1FFE, Dec=8190
Drive=drive4, Hex=980B, Dec=38923
Drive=drive5, Hex=0011780c, Dec=1144844

real    0m0.042s
user    0m0.011s
sys     0m0.065s

$ for i in `perl -e '$,=" "; print 1..1000'`
do
    cat hex.input
done > hex.big

$ wc -l hex.big
    5000 hex.big

$ time ./hex2dec-sh.sh hex.big > /dev/null

real    0m32.141s
user    0m9.131s
sys     0m56.458s

What if I have a few million lines to convert? You will need a lot of coffee breaks. The alternative is to use some high level scripting languages, such as Perl, Python, Tcl, ... In fact you can crank out your own function in AWK to do this kind of thing. Here I will show you a Perl one-liner and the AWK way.

Perl one-liner:

$ time perl -ne 'chomp();@l=split(/\s+/);print "Drive=",$l[0]," Hex=",$l[1]," Dec=",hex($l[1]),"\n"' < hex.input
Drive=drive1 Hex=0089 Dec=137
Drive=drive2 Hex=0a2f Dec=2607
Drive=drive3 Hex=1FFE Dec=8190
Drive=drive4 Hex=980B Dec=38923
Drive=drive5 Hex=0011780c Dec=1144844

real    0m0.008s
user    0m0.003s
sys     0m0.005s

$ time perl -ne 'chomp();@l=split(/\s+/);print "Drive=",$l[0]," Hex=",$l[1]," Dec=",hex($l[1]),"\n"' < hex.big  > /dev/null

real    0m0.044s
user    0m0.038s
sys     0m0.005s

AWK way:

$ cat hex2dec-awk.sh
#! /bin/sh


if [ $# -ne 1 ]; then
        echo "Usage: $0 <hex-input>"
        exit 1
fi


nawk '
function hex2dec(hex, h, i, factor, n, sum) {
        n = length(hex)
        factor = 1
        sum = 0
        for ( i=n ; i>0 ; --i ) {
                h = substr(hex, i, 1)
                if ( h == "a" || h == "A" ) { h=10 }
                if ( h == "b" || h == "B" ) { h=11 }
                if ( h == "b" || h == "B" ) { h=11 }
                if ( h == "c" || h == "C" ) { h=12 }
                if ( h == "d" || h == "D" ) { h=13 }
                if ( h == "e" || h == "E" ) { h=14 }
                if ( h == "f" || h == "F" ) { h=15 }
                sum += factor * h
                factor *= 16
        }
        return sum
}
{
        printf("Drive=%s, Hex=%s, Dec=%s\n", $1, $2, hex2dec($2))
}' $1

$ time ./hex2dec-awk.sh hex.input
Drive=drive1, Hex=0089, Dec=137
Drive=drive2, Hex=0a2f, Dec=2607
Drive=drive3, Hex=1FFE, Dec=8190
Drive=drive4, Hex=980B, Dec=38923
Drive=drive5, Hex=0011780c, Dec=1144844

real    0m0.008s
user    0m0.002s
sys     0m0.006s

$ time ./hex2dec-awk.sh hex.big > /dev/null 2>&1

real    0m0.100s
user    0m0.093s
sys     0m0.006s

For 5000 lines, you can reduce it from 32 seconds run time down to sub second. I am sure you can see the performance differences in the 3 implementations. Next time, think big! Bigger data size.

Labels: , , ,

Monday, April 06, 2009

Avoiding all the banners

Sometime you need to automate your activities by ssh into a remote host and sudo to a particular user to execute a command. Although all the authentication has been setup to be passwordless, the output include those extra banners (/etc/motd and /etc/issue) which you normally want to get rid of.

There is one nice trick that you may want to adopt. By crafting a unique string in your script, you can use that as your marker to separate the banners from your command output. So your ssh sudo output can pipe through awk to separate out unwanted banners.

unique="=-a-b-c-="

ssh chihung@$remote "sudo su - someuser -c 'echo $unique;somecmd'" 2>&1 | \
    awk '/^'$unique'$/ { start=1; next } start==1 { print }'

Make sure your $unique get substituted in your current shell by enclosing them in double quotes. In this approach, you do not have to worry about how the banners look like and you do not have to do grep -v some-banner-string

Labels: ,

Sunday, March 29, 2009

Four Ways to Pass Shell Variables in AWK

As we all know, not everyone is equal. This applies to AWK too. AWK in Solaris is a very old implementation compared the GNU AWK.

Here I am trying to show you 4 different ways to pass shell variable value to AWK

  1. This trick works on all flavours of AWK because it is taking advantage of shell substitution. Remember not to leave any space when you include a single quote, dollar variable, and a single quote in the AWK command
    $ one=111
    
    $ two=222
    
    $ awk 'BEGIN{a='$one';b='$two'}END{print a,b}' /dev/null
    111 222
    
  2. This works for all flavours of AWK too. AWK allows you to set their variable from the shell
    $ one=111
    
    $ two=222
    
    $ awk 'END{print a,b}' a=$one b=$two /dev/null
    111 222
    
  3. This will not work for Solaris awk. You have to use nawk. The -v flag allows you to assign AWK variable.
    $ one=111
    
    $ two=222
    
    $ nawk -v a=$one -v b=$two 'END{print a,b}' /dev/null
    111 222
    
  4. If your awk allows you to access the shell environment variables, you can use this trick. FYI, this will not work for Solaris awk.
    $ one=111
    
    $ two=222
    
    $ a=$one b=$two awk 'END{print ENVIRON["a"],ENVIRON["b"]}' /dev/null
    111 222
    

Labels: ,

Tuesday, March 24, 2009

AWK Can Do Lookup, with ease

Some time ago, I blogged about AWK can do lookup. In fact, you can handle that with ease if your AWK supports FNR. FNR is the input record number in the current input file. If the first filename to AWK contains the lookup mapping, you can get AWK to store the lookup if FNR==NR. FNR==NR is always true for the first file processed by AWK. For subsequent files, FNR will be reset to 1 and therefore FNR==NR condition will no longer be true.

Below simple example demonstrates the power of FNR

file1.txt contains the lookup mapping and lookup will be applied to file2.txt and file3.txt

$ cat file1.txt
1 First
2 Second
3 Third
4 Fourth
5 Fifth

$ cat file2.txt
swimming 1
table-tennis 3
cycling 2
running 1
tennis 3

$ cat file3.txt
perl 4
python 3
tcl 2
shell-script 2

$ awk 'FNR==NR{map[$1]=$2;next}{print $1,map[$2]}' file1.txt file2.txt file3.txt
swimming First
table-tennis Third
cycling Second
running First
tennis Third
perl Fourth
python Third
tcl Second
shell-script Second

FYI, Solaris /usr/bin/awk does not support FNR and you have to use nawk

Labels:

Friday, December 05, 2008

Avoid Using Temporary Files, Part 2

Last Saturday, I blogged about how we can avoid using temporary files in shell scripting. At the end of that blog, I posted a question - how we can achieve this if the number of lines in all the command outputs are not the same.

My first implementation started off with two commands with unequal output and I managed to do that without much difficulty. I thought I was done with this. Wait! What if there more than 2 commands output, that means I have to rewrite this again. Why not we do it once and for all, craft a more generic function that is able to handle multiple command outputs.

My approach is to take advantage of sub shell. Also, I will introduce a "separator" in-between the commands so that the "paste" will be able to separate the output. Of course your "separator" has to be unique and it will not appear in any of the command output. I define my own "_paste_" command using AWK and store the commands output in memory using AWK associate array with the key based on "#file and #line". Here is my code:

$ cat t3.sh
#! /bin/sh

PATH=/usr/bin:/bin

seq()
{
    nawk -v start=$1 -v end=$2 '
        END {for(i=start;i<=end;++i){print i}}' /dev/null
}
calc1()
{
    for i in `seq $1 $2`
    do
        echo `expr $i \* $i`
    done
}
calc2()
{
    for i in `seq $1 $2`
    do
        echo `expr $i \* $i \* $i`
    done
}
_paste_()
{
    nawk -v sep=$sep '
        BEGIN {
            nfile=1
            nline=1
            max=0
        }
        $0==sep {
            ++nfile
            nline=1
            next
        }
        {
            if ( nline>max ) {
                max=nline
            }
            line[nfile,nline]=$0
            ++nline
        }
        END {
            for (l=1;l<=max;++l) {
                printf("%s", line[1,l])
                for (f=2;f<=nfile;++f) {
                    printf("\t%s", line[f,l])
                }
                printf("\n")
            }
        }'
}


sep="@@@@@"
(
   calc1 1 10; echo $sep
   calc2 1 13; echo $sep
   calc2 1 15
) | _paste_ 

$ ./t3.sh
1       1       1
4       8       8
9       27      27
16      64      64
25      125     125
36      216     216
49      343     343
64      512     512
81      729     729
100     1000    1000
        1331    1331
        1728    1728
        2197    2197
                2744
                3375

This implementation may not be very efficient especially if we have to deal with massive output from commands because all the data will be stored in memory. What I have in mind is to do this in Python, wanna give it a try?

Labels: ,

Tuesday, December 02, 2008

Turned A No-Brainer Task Into A Challenging Job

Yesterday I was tasked to write up a capacity report that was used to be carried out by the administrative staff. The instruction given to the admin staff is to look into the weekly graph (generated by RRDtool) and choose a busy day with the highest CPU utilisation. Once the date has been identified, he/she will have to view that particular day's CPU graph. If the utilisation is above certain threshold consecutively within a pre-defined period, the server will be classified as either Amber or Red depending on the threshold level.

Yes, this is a no brainer job. However, if you were to do it for near to hundred servers, a no brainer job will become a nerve cracking job. Sooner or later you will swear like hell. BTW, I did swear too. After all the swearing, I was wondering whether I can do a better job than what the admin staff used to do. I cannot possibly doing this manually every month, right?

After some exploratory works and understanding how the files store these information, I realised that I should be able to do that programmatically by dumping the RRD files into ASCII text, and in this case is in XML format. My next question will be, shall I use XML parser to extract the information ? But not for this case because the system does not have any XML toolkit installed. Also some of the XML toolkit may filter off the comments which I will need to tap onto (the timestamp in yyyy-mm-dd format and epoch time). This is a very useful piece of information to determine whether the server is "amber" or "red".

I always belief that I can extract anything as long as the output is generated by a program, it ought to have a pattern. Here I am showing you a dump of the RRD (at the end of this blog), can you see the pattern ?

I will not show you my code because it is rather involve and messy. However, I will describe my approach in getting things done. Basically I use a lot of UNIX pipes between a mixture of AWK and sed. FYI, I avoided using temporary file for all the processing

  • In my case, 1st <datasbase> stores the daily info, 2nd for weekly, 3rd for monthly and 4th for yearly (depending on how you create your RRD)
  • Use AWK/sed to pick up the data from 2nd <database>, ignore the NaN (not a number) record, extract the timestamp
  • Pipe that into another AWK to work out which date in the week has the highest CPU utilisation
  • Open up that day's RRD (apparently it is stored in another RRD)
  • Retrieve the 1st <database> data, that's the daily data
  • Work out the time difference between those records that are above the threshold
  • Count those records above the thresholds. Suppose the polling interval is 5 minutes, we should be seeing a continuous 300 seconds time difference in the filtered records.
  • If count exceeds the time specified (continuous 1 hour means 12 data points), we flag it out as either Amber or Red depending on the threshold

I hope you are still with me. The moral of the story is not about the above steps, it is about we should always try to find joy in doing our work no matter how dump it is. It looked like a no-brainer job at first, but at the end it turned out be pretty challenging one.

Here is the RRD file dump:

$ rrdtool dump some-rrd-file.rrd
<!-- Round Robin Database Dump -->
<rrd>
    <version> 0001 </version>
    <step> 15 </step> <!-- Seconds -->
    <lastupdate> 1222743000 </lastupdate> <!-- 2008-09-30 10:50:00 SGT -->

    <ds>
        <name> ds0 </name>
        <type> GAUGE </type>
        <minimal_heartbeat> 600 </minimal_heartbeat>
        <min> 0 </min>
        <max> 1.0000000e+02  </max>

        <!-- PDP Status -->
        <last_ds> 4.3240000e+01 </last_ds>
        <value> 0.0000000000e+00 </value>
        <unknown_sec> 0 </unknown_sec>
    </ds>

<!-- Round Robin Archives -->
    <rra>
        <cf> AVERAGE </cf>
        <pdp_per_row> 1 </pdp_per_row> <!-- 300 seconds -->
        <xff> 5.0000000000e-01 </xff>

        <cdp_prep>
            <ds><value> NaN </value>  <unknown_datapoints> 0 </unknown_datapoints></ds>
        </cdp_prep>
        <database>
            <!-- 2008-10-24 09:50:00 SGT / 1224813000 --> <row><v> 1.5000000e+01 </v></row>
            <!-- 2008-10-24 09:55:00 SGT / 1224813300 --> <row><v> 1.0234000e+01 </v></row>
            .....
        </database>
    </rra>
    <rra>
        ....
        <database>
            <!-- 2008-10-20 12:00:00 SGT / 1224475200 --> <row><v> 3.1365000e+01 </v></row>
            <!-- 2008-10-20 12:30:00 SGT / 1224477000 --> <row><v> 2.4532000e+01 </v></row>
            .....
        </database>
    </rra>
    <rra>
        ....
        <database>
            .....
        </database>
    </rra>
</rrd>

Labels: , , ,

Saturday, November 29, 2008

Avoid Using Temporary Files

If your style of writing shell script is usually based on outputting stuff to temporary files for future processing, I can tell you that in most cases it is possible to do the same job without writing anything to the OS simply by using some UNIX shell tricks. By doing this, your script will be more efficient and more portable.

Suppose you have two functions (or commands) that produce SAME number of lines of output and you want to 'paste' the two output together. The easiest way out is to store the output in separate files. In this blog, I will introduce two functions, namely calc1 (calculate n*n) and calc2 (calculate n*n*n).

$ cat t0.sh
#! /bin/sh

PATH=/usr/bin:/bin

seq()
{
        nawk -v start=$1 -v end=$2 '
                END {for(i=start;i<=end;++i){print i}}' /dev/null
}
calc1()
{
        for i in `seq $1 $2`
        do
                echo `expr $i \* $i`
        done
}
calc2()
{
        for i in `seq $1 $2`
        do
                echo `expr $i \* $i \* $i`
        done
}

calc1 1 10 > sometempfile1
calc2 1 10 > sometempfile2
paste sometempfile1 sometempfile2
rm -f sometempfile1 sometempfile2

$ ./t0.sh
1       1
4       8
9       27
16      64
25      125
36      216
49      343
64      512
81      729
100     1000

In this scenario, the output from calc1 and calc2 are having the same number of records. We can simply take advantage of this by combining the output using UNIX sub-shell and have the output to be handled by AWK. In the AWK, I will store the output in an associative array (line) based on the record number (NR) and the array will be processed at the END block.

$ cat t1.sh
#! /bin/sh

PATH=/usr/bin:/bin

seq()
{
        nawk -v start=$1 -v end=$2 '
                END {for(i=start;i<=end;++i){print i}}' /dev/null
}
calc1()
{
        for i in `seq $1 $2`
        do
                echo `expr $i \* $i`
        done
}
calc2()
{
        for i in `seq $1 $2`
        do
                echo `expr $i \* $i \* $i`
        done
}

# using sub shell to group the output
( calc1 1 10 ; calc2 1 10) | \
nawk '
{ line[NR]=$0 }
END {
        for(i=1;i<=NR/2;++i) {
                print line[i] "\t" line[NR/2+i]
        }
}'

$ ./t1.sh
1       1
4       8
9       27
16      64
25      125
36      216
49      343
64      512
81      729
100     1000

As I mentioned earlier on, one can accomplish the same task without temporary files.

Wait, the task has not finished yet. What if the output records are not the same ?

( calc1 1 10 ; calc2 1 13 ) | ...
Obviously the second script will break. Can you fix it for me ? Do give it a try and I will provide my solution in a couple of days time.

Labels: ,

Wednesday, October 29, 2008

Difficult NAWK to Understand

I have not been involved in the UNIX.com shell programming forum for almost a month. Today I received an email reminder from the forum administrator. One of the questions that caught my attention is this nawk one-liner:
I found a command who prints x lines before and after a line who contain a searched string in a text file. The command is :
nawk 'c-->0;$0~s{if(b)for(c=b+1;c>1;c--)print r[(NR-c+1)%b];print;c=a}b{r[NR%b]=$0}' b=2 a=4 s="string" file1
It works very well but I can't understand the syntax, too difficult with "man nawk". Is that some one who will be able to comment this syntax ?

The one-liner is using a lot of shortcut and defaults in the awk code and make it so cryptic. My 'deciphered' version:

nawk -v before=4 -v after=4 -v search="string" '
--current > 0 {
        print
}
$0 ~ search{
        if ( before ) {
                for ( current=before+1 ; current>1 ; current-- ) {
                        print rec[(NR-current+1)%before]
                }
        }
        print
        current=after
}
before{
        rec[NR%before]=$0
}' file1

I think the code is now pretty self-explanatory, hopefully :-)

Labels:

Thursday, September 25, 2008

Paste a Few Files Into One and Plot ...

Suppose you have been monitoring a few parameters of a service periodically and output them in separate files. Each file format is colon separated with key value pair, and the key is in the timestamp YYYYmmddHHMMSS format. Now you are required to plot a few of these parameters together in a single plot.

Example:
File 1 Line 1 = 20080910111213:14
File 2 Line 1 = 20080910111213:11
File 3 Line 1 = 20080910111213:23
and you are required to put them together in one line so that you can reuse your generic plotting tool to visualise all these values in a single graph.
Output Line 1 = 20080910111213:14:11:23

Couple of assumption in this work:

  • data in each file is colon separated with
    1st field=time stamp format in YYYYmmddHHMMSS
    2nd field=value
  • all files are having the same time stamp in each corresponding row
Here is the script:
#! /bin/sh

if [ $# -lt 2 ]; then
 echo "Usage: $0 file file [file ...]"
 exit 1
fi


#
# assumptions:
# 1. assume data in each file is colon separated with
#    1st field: time stamp format in YYYYmmddHHMMSS
#    2nd field: value
#    Eg. 20080909121314:2
# 2. all files are having the same time stamp in each corresponding row
#


prefix=".tmprrd-$$"
sep=":"


#
# modify the time stamp for easy parsing in Tcl (clock scan)
#
count=1
suffix=`echo $count | awk '{printf("%03d",$1)}'`
awk -F"$sep" '{printf("%sT%s:%d\n",substr($1,1,8),substr($1,9,6),$2)}' $1 > \
 ${prefix}-${suffix}
count=`expr $count + 1`


#
# for each file (starting from 2nd), extract 2nd field to individual file
#
shift
for i in $@
do
 suffix=`echo $count | awk '{printf("%03d",$1)}'`
 awk -F"$sep" '{print $2}' $i > ${prefix}-${suffix}
 count=`expr $count + 1`
done


#
# 'paste' them together
#
paste -d "$sep" ${prefix}-*


#
# cleanup
#
rm -f ${prefix}-*

As you can see, I modified the time stamp from the first file and stored it in some temporary file with a suffix of "*-001". The time stamp has been reformatted to an acceptable format by Tcl clock scan because the generic plotting tool is implemented in Tcl. As for the values in each file (starting from the 2nd file 'cos the first one has been taken care of), I output them separately with suffix having 3 digit (zero padded) running number. This allows me to take advantage of shell wild card to ensure the sequence of the values corresponds to the sequence of the input files. With this, I can just paste ${prefix}-*.

The output is a beautiful graph.

Labels: , , ,

Saturday, August 23, 2008

Summarise Netstat Inbound and Outbound Traffic

If we are managing a busy server with tonnes of inbound and outbound network connections, the below script may help you to summarise the 'netstat' details.

If connection tuple's (localhost:localport - remotehost:remoteport) localport is one of the listening ports, we can say that the connection is an inbound, else outbound. At first I tried to use ephemeral port to figure out the in/out bound, but find it not very reliable 'cos software can listen to a high port too. Also, you need to run netstat with -n (Show network addresses as numbers) in order to figure out the numeric port number.

The "getlisten" shell function is a trick that I normally used to dynamically create the AWK BEGIN content. In this case, I parse the "netstat" output and set the "listen" array variable in AWK to 1. This function will be invoked by the shell (not inside AWK) and shell will 'glue' them together with the AWK code. '`getlisten`' - first single quote is to temporary terminate AWK, follow by backquote to run the shell function, then open a single quote to continue the AWK. Remember no space is allowed between the single quote and backquote.

#! /bin/sh
#
# Summary netstat information by inbound and outbound traffic




TMPFILE=/tmp/.netstat-$$
trap "rm -f $TMPFILE" 0 1 2 3 9 15


netstat -a -finet -Ptcp > $TMPFILE
timestamp=`date '+%Y%m%dT%H%M%S'`


#
# shell function to create AWK BEGIN block for all the listening ports
# eg. listen["123"]=1;
#
getlisten()
{
 awk '$NF=="LISTEN" {n=split($1,a,".");printf("listen[\"%s\"]=1;",a[n])}' $TMPFILE
}



nawk -v timestamp=$timestamp '

function getport (hostport, n) {
 n=split(hostport,a,".")
 return a[n]
}

function gethost (hostport, n, h) {
 n=split(hostport,a,".")
 h=a[1]
 for(i=2;i<n;++i) {
  h=sprintf("%s.%s",h,a[i])
 }
 return h
}

BEGIN {'`getlisten`'}

NR>4 && $NF!~/(IDLE|BOUND|LISTEN)$/ {
 lh=gethost($1)
 lp=getport($1)
 rh=gethost($2)
 rp=getport($2)

 # if local port is one of the listen ports
 #   is inbound
 # else
 #   is outbound
 if ( listen[lp] == 1 ) {
  key=sprintf("%s:%s<-%s %s ",lh,lp,rh,$NF)
  ++inbound[key]
 } else {
  key=sprintf("%s->%s:%s %s ",lh,rh,rp,$NF)
  ++outbound[key]
 }
}

END {
 for(i in inbound) {
  print timestamp, i, inbound[i]
 }
 for(i in outbound) {
  print timestamp, i, outbound[i]
 }
}
' $TMPFILE

Sample output:

$ ./n.sh
20080822T163621 sgehost:sge_qmaster<-sgehost ESTABLISHED  1
20080822T163621 sgehost:ldap<-sgehost ESTABLISHED  12
20080822T163621 sgehost:ssh<-remote_server ESTABLISHED  1
20080822T163621 sgehost:sge_qmaster<-sgeexec2 ESTABLISHED  1
20080822T163621 sgehost:sge_qmaster<-sgeexec0 ESTABLISHED  1
20080822T163621 sgehost:sge_qmaster<-sgeexec1 ESTABLISHED  1
20080822T163621 sgehost->sgehost:ldap ESTABLISHED  12
20080822T163621 sgehost->sgehost:sge_qmaster ESTABLISHED  1

BTW, this script is developed on a Solaris platform.

Labels: , ,

Monday, July 28, 2008

An Old Task in Python

Finding a real problem to brush up my skill on Python is not an easy task. Instead of waiting for a new problem to come, I look for old problem that I still have the input data set. This also enables me to compare Python with my previous solution.

Couple of months ago, my colleague passed me IIS web access log from a rather busy web server. I managed to extract the session concurrency information and visualised the result using Gnuplot. This trick was derived some years ago when I was doing a performance testing project. Basically I extracted all the timestamps for individual session ID, assuming that the session IDs are unique. It is possible to 'stack up' all the sessions by increment the per second counter between the start and end of the session duration. The end result will be the session concurrency.

The input data is a 170MB web access log with 227K lines of log based on a single day web traffic. My previous solution was based on AWK script and the run time was 3min 6sec. With Python 2.5.2, the run time is 20 sec, almost 10 folds in performance gain.

Here is my python script:

#! /usr/bin/python

import datetime
import time
import sys


if len(sys.argv) != 2:
 print "Usage:", sys.argv[0], "<web-log>"
 exit(1)


# convert string to integer, leading zeros are stripped
# '00'->0, '08'->8, '11'->11
def str2int(s):
 t=s.lstrip('0')
 if t=='': t=0
 return int(t)


# determine epoch from web access log timestamp
# 2008-07-21 00:00:07 myserver 1.2.3.4 GET / .....
def findEpoch(line):
 yr=str2int(line[0:4])
 mth=str2int(line[5:7])
 day=str2int(line[8:10])
 hh=str2int(line[11:13])
 mm=str2int(line[14:16])
 ss=str2int(line[17:19])
 t=datetime.datetime(yr,mth,day,hh,mm,ss)
 epoch=int(time.mktime(t.timetuple()))
 return epoch


sessions={}
concurrency={}


# start time of log
# read first line (no need to determine first line in for loop)
fp=open(sys.argv[1],"r")
line=fp.readline()
starttime=findEpoch(line)
fp.close()


sess="PROD_JSESSION_UID"
sessN=len(sess)
for line in open(sys.argv[1],"r"):
  
 cookie=line.rstrip().split(' ')[12]
 if sess in cookie:

  epoch=findEpoch(line)


  # get session id
  i1=cookie.index(sess)
  try:
   i2=cookie.index(";",i1)
  except:
   i2=len(cookie)
  uid=cookie[i1+sessN+2:i2]


  # store sessions
  try:
   sessions[uid]="%s,%s" % (sessions[uid],str(epoch))
  except:
   sessions[uid]="%s" % str(epoch)

endtime=epoch


# initialise to zero
count=starttime
while count<=endtime:
 concurrency[count]=0
 count+=1


# add up concurrency for all sessions
for key in sessions.keys():
 ltime=sessions[key].split(',')
 t0=int(ltime[0])
 t1=int(ltime[-1])
 count=t0
 while count<=t1:
  concurrency[count]+=1
  count+=1


count=starttime
while count<=endtime:
 print count,concurrency[count]
 count+=1

The plot from Python looked the same as the AWK-based program:

Labels: , ,

Monday, June 30, 2008

Range Pattern in AWK

I came across this post regarding removing #ifdef code from C program. Although the thread is closed for no obvious reason, it is pretty interesting from the shell script view point. The original post is like this:
Removing lines of code defined under flag

I want to write a shell script that will remove lines of C code that is defined under a certain flag, for eg, "#ifdef PRODUCT" in all the C files in a directory. Please help me somebody..I'm clueless!!

In C compiler, you should be able to run through the pre-processor (-E flag) to ignore the "#ifdef PROD" code by defining all the other conditional compilation directives with the "-D" flag. However, the preprocessing also include the content of the header files. This is definitely not the solution we are looking for.

In AWK, you can specify a range pattern and apply an action statement. Suppose we have this simple.c C program and our job is to remove the #ifdef PROD block of code.

#include <stdio.h>

int main(void) {

        #ifdef PROD
        printf("Prod is here\n");
        #endif

        #ifdef DEV
        printf("Dev is here\n");
        #endif

        #ifdef PROD
        printf("Prod is there\n");
        #endif
}

Below AWK code skips to the next input record when it is within the range pattern. The start range pattern is any whitespace followed by #ifdef PROD and the end range pattern is any whitespace followed by #endif. Otherwise, it just simply print it out.

$ awk '
/^[ \t]*#ifdef PROD/,/^[ \t]*#endif/ {
next
}
{
print
}' sample.c
#include <stdio.h>

int main(void) {


        #ifdef DEV
        printf("Dev is here\n");
        #endif

}

Range pattern in AWK is particularly useful and very often people deliberately plant their own start and end patterns in their data stream for future processing. May be you can start to think along this line.

Labels:

Tuesday, June 17, 2008

How to Parse Multiline in AWK

I was given a Java thread dump output from my colleague and the output is kind of a multiline records with record separator (RS) being a blank line. The question is, can AWK parse this type of file just like other space/colon/comma separated files. The answer is yes. It is well documented in The AWK Programming Language written by the three authors. If you have the book, it is described on page 60-61.

Here is a sample script to demonstrate how we can summarise the "Thread-" name that is in waiting and/or in lock state. The sample Java thread dump is download from here.

$ cat threaddump.sh
#! /bin/sh

if [ $# -ne 1 ]; then
 echo "Usage: $0 <thread-dump>"
 exit 1
fi
if [ ! -f $1 ]; then
 echo "Error: $1 does not exist"
 exit 2
fi


awk '
BEGIN {
 RS=""
 FS="\n"
}
/^"Thread-/ {
 split($1,a," ")
 if ( /waiting on/ ) {
  wait[a[1]]++
 }
 if ( /locked/ ) {
  lock[a[1]]++
 }
}
END {
 printf("Thread\t\tWait\tLock\n")
 printf("------\t\t----\t----\n")
 for ( i in wait ) {
  printf("%s\t%d\t%d\n", i, wait[i], lock[i])
 }
}' $1


$ cat threaddump.txt
Full thread dump Java HotSpot(TM) Client VM (1.5.0_10-b03 mixed mode):

"Thread-7" prio=4 tid=0x0b482220 nid=0x1570 in Object.wait() [0x0bbcf000..0x0bbcfae8]
 at java.lang.Object.wait(Native Method)
 - waiting on <0x03017960> (a concurrency.diners.Fork)
 at java.lang.Object.wait(Object.java:474)
 at concurrency.diners.Fork.get(Fork.java:22)
 - locked <0x03017960> (a concurrency.diners.Fork)
 at concurrency.diners.Philosopher.run(Philosopher.java:29)

"Thread-6" prio=4 tid=0x0b481808 nid=0xa84 in Object.wait() [0x0bb8f000..0x0bb8fb68]
 at java.lang.Object.wait(Native Method)
 - waiting on <0x030707e0> (a concurrency.diners.Fork)
 at java.lang.Object.wait(Object.java:474)
 at concurrency.diners.Fork.get(Fork.java:22)
 - locked <0x030707e0> (a concurrency.diners.Fork)
 at concurrency.diners.Philosopher.run(Philosopher.java:29)

"Thread-5" prio=4 tid=0x0b47e310 nid=0x167c in Object.wait() [0x0bb4f000..0x0bb4fbe8]
 at java.lang.Object.wait(Native Method)
 - waiting on <0x03070850> (a concurrency.diners.Fork)
 at java.lang.Object.wait(Object.java:474)
 at concurrency.diners.Fork.get(Fork.java:22)
 - locked <0x03070850> (a concurrency.diners.Fork)
 at concurrency.diners.Philosopher.run(Philosopher.java:29)

"Thread-4" prio=4 tid=0x0b47d808 nid=0x1730 in Object.wait() [0x0bb0f000..0x0bb0fc68]
 at java.lang.Object.wait(Native Method)
 - waiting on <0x030708c0> (a concurrency.diners.Fork)
 at java.lang.Object.wait(Object.java:474)
 at concurrency.diners.Fork.get(Fork.java:22)
 - locked <0x030708c0> (a concurrency.diners.Fork)
 at concurrency.diners.Philosopher.run(Philosopher.java:29)

"Thread-3" prio=4 tid=0x0b480cd8 nid=0x11c4 in Object.wait() [0x0bacf000..0x0bacfce8]
 at java.lang.Object.wait(Native Method)
 - waiting on <0x03017b38> (a concurrency.diners.Fork)
 at java.lang.Object.wait(Object.java:474)
 at concurrency.diners.Fork.get(Fork.java:22)
 - locked <0x03017b38> (a concurrency.diners.Fork)
 at concurrency.diners.Philosopher.run(Philosopher.java:29)

"AWT-EventQueue-1" prio=4 tid=0x0b46e1d0 nid=0x16c8 in Object.wait() [0x0ba4f000..0x0ba4fa68]
 at java.lang.Object.wait(Native Method)
 - waiting on <0x02ffa368> (a java.awt.EventQueue)
 at java.lang.Object.wait(Object.java:474)
 at java.awt.EventQueue.getNextEvent(EventQueue.java:345)
 - locked <0x02ffa368> (a java.awt.EventQueue)
 at java.awt.EventDispatchThread.pumpOneEventForHierarchy(EventDispatchThread.java:189)
 at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:163)
 at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:157)
 at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:149)
 at java.awt.EventDispatchThread.run(EventDispatchThread.java:110)

"DestroyJavaVM" prio=6 tid=0x00266dc0 nid=0x1118 waiting on condition [0x00000000..0x0006fae8]

"AWT-EventQueue-0" prio=6 tid=0x0b451f60 nid=0x124c in Object.wait() [0x0b82f000..0x0b82fbe8]
 at java.lang.Object.wait(Native Method)
 - waiting on <0x0300a858> (a java.awt.EventQueue)
 at java.lang.Object.wait(Object.java:474)
 at java.awt.EventQueue.getNextEvent(EventQueue.java:345)
 - locked <0x0300a858> (a java.awt.EventQueue)
 at java.awt.EventDispatchThread.pumpOneEventForHierarchy(EventDispatchThread.java:189)
 at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:163)
 at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:157)
 at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:149)
 at java.awt.EventDispatchThread.run(EventDispatchThread.java:110)

"thread applet-concurrency/diners/Diners.class" prio=4 tid=0x0b3cab40 nid=0x10a0 in Object.wait() [0x0b7ef000..0x0b7efb68]
 at java.lang.Object.wait(Native Method)
 - waiting on <0x0300a900> (a sun.applet.AppletViewerPanel)
 at java.lang.Object.wait(Object.java:474)
 at sun.applet.AppletPanel.getNextEvent(AppletPanel.java:282)
 - locked <0x0300a900> (a sun.applet.AppletViewerPanel)
 at sun.applet.AppletPanel.run(AppletPanel.java:332)
 at java.lang.Thread.run(Thread.java:595)

"AWT-Windows" daemon prio=6 tid=0x0ac90b38 nid=0x1124 runnable [0x0af0f000..0x0af0fce8]
 at sun.awt.windows.WToolkit.eventLoop(Native Method)
 at sun.awt.windows.WToolkit.run(WToolkit.java:269)
 at java.lang.Thread.run(Thread.java:595)

"AWT-Shutdown" prio=6 tid=0x0ac90780 nid=0x7dc in Object.wait() [0x0aecf000..0x0aecfd68]
 at java.lang.Object.wait(Native Method)
 - waiting on <0x02fb5830> (a java.lang.Object)
 at java.lang.Object.wait(Object.java:474)
 at sun.awt.AWTAutoShutdown.run(AWTAutoShutdown.java:259)
 - locked <0x02fb5830> (a java.lang.Object)
 at java.lang.Thread.run(Thread.java:595)

"Java2D Disposer" daemon prio=10 tid=0x0ac82aa8 nid=0x1014 in Object.wait() [0x0ae8f000..0x0ae8f9e8]
 at java.lang.Object.wait(Native Method)
 - waiting on <0x02fdd7a8> (a java.lang.ref.ReferenceQueue$Lock)
 at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:116)
 - locked <0x02fdd7a8> (a java.lang.ref.ReferenceQueue$Lock)
 at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:132)
 at sun.java2d.Disposer.run(Disposer.java:107)
 at java.lang.Thread.run(Thread.java:595)

"Low Memory Detector" daemon prio=6 tid=0x00a94e70 nid=0x1038 runnable [0x00000000..0x00000000]

"CompilerThread0" daemon prio=10 tid=0x00a93a70 nid=0x12c8 waiting on condition [0x00000000..0x0abcf8c8]

"Signal Dispatcher" daemon prio=10 tid=0x00a92e28 nid=0x16bc waiting on condition [0x00000000..0x00000000]

"Finalizer" daemon prio=8 tid=0x00a89cd0 nid=0x1044 in Object.wait() [0x0ab4f000..0x0ab4fc68]
 at java.lang.Object.wait(Native Method)
 - waiting on <0x02fdd950> (a java.lang.ref.ReferenceQueue$Lock)
 at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:116)
 - locked <0x02fdd950> (a java.lang.ref.ReferenceQueue$Lock)
 at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:132)
 at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159)

"Reference Handler" daemon prio=10 tid=0x00a88860 nid=0x106c in Object.wait() [0x0ab0f000..0x0ab0fce8]
 at java.lang.Object.wait(Native Method)
 - waiting on <0x02fdd700> (a java.lang.ref.Reference$Lock)
 at java.lang.Object.wait(Object.java:474)
 at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116)
 - locked <0x02fdd700> (a java.lang.ref.Reference$Lock)

"VM Thread" prio=10 tid=0x00a85d98 nid=0x1030 runnable 

"VM Periodic Task Thread" prio=10 tid=0x00a960c8 nid=0x14b4 waiting on condition 


$ ./threaddump.sh threaddump.txt
Thread          Wait    Lock
------          ----    ----
"Thread-3"      1       1
"Thread-4"      1       1
"Thread-5"      1       1
"Thread-6"      1       1
"Thread-7"      1       1

I can tell you that AWK is damn powerful.

Labels: ,

Monday, June 16, 2008

My First Python Program

This is my second attempt in trying to learn Python since 2000. You may be wondering what's the motivation behind it and whether I will "dump" my favourite scripting, Tcl, to go full steam with Python. Tcl is still my "mother tongue" and definitely no harm to learn another "foreign language".

The motivation comes from "The Zen of Python" and the way they do multi-precision integer calculation. Below shows Python in action and compare with Perl (with Bignum module) & UNIX bc:

$ /cygdrive/c/Python25/python
Python 2.5.2 (r252:60911, Feb 21 2008, 13:11:45) [MSC v.1310 32 bit (Intel)] on
win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
>>> 2**1000
10715086071862673209484250490600018105614048117055336074437503883703510511249361
22493198378815695858127594672917553146825187145285692314043598457757469857480393
45677748242309854210746050623711418779541821530464749835819412673987675591655439
46077062914571196477686542167660429831652624386837205668069376L
>>>exit()

$ perl -v

This is perl, v5.8.8 built for cygwin-thread-multi-64int
(with 8 registered patches, see perl -V for more detail)

Copyright 1987-2006, Larry Wall

Perl may be copied only under the terms of either the Artistic License or the
GNU General Public License, which may be found in the Perl 5 source kit.

Complete documentation for Perl, including FAQ lists, should be found on
this system using "man perl" or "perldoc perl".  If you have access to the
Internet, point your browser at http://www.perl.org/, the Perl Home Page.

$ echo "use Bignum; print 2**1000" | perl
1.07150860718627e+301

$ echo "2^1000" | bc
10715086071862673209484250490600018105614048117055336074437503883703\
51051124936122493198378815695858127594672917553146825187145285692314\
04359845775746985748039345677748242309854210746050623711418779541821\
53046474983581941267398767559165543946077062914571196477686542167660\
429831652624386837205668069376

Recently my colleague passed me a pretty big (170MB in size, 227K lines) IIS log file and I thought this is a good time to practice my Python skill. BTW, this is my first not so trivial Python program. The objective of the program is to work out the hourly byte sent, byte received and hits. Also, I wanted to compare Python with AWK and Tcl (8.4.12).

Here is the "battle field" for Python vs Tcl vs AWK in my Cygwin. To be fair, each program will be executed 3 times.

$ time ./sum.py iis.log > a

real    0m14.672s
user    0m0.015s
sys     0m0.015s

$ time ./sum.py iis.log > a

real    0m15.391s
user    0m0.031s
sys     0m0.031s

$ time ./sum.py iis.log > a

real    0m15.094s
user    0m0.015s
sys     0m0.031s

$ time ./sum.sh iis.log > b

real    0m18.704s
user    0m15.170s
sys     0m0.373s

$ time ./sum.sh iis.log > b

real    0m18.219s
user    0m14.951s
sys     0m0.233s

$ time ./sum.sh iis.log > b

real    0m18.390s
user    0m14.873s
sys     0m0.483s

$ time ./sum.tcl iis.log > c

real    0m15.781s
user    0m0.015s
sys     0m0.015s

$ time ./sum.tcl iis.log > c

real    0m14.641s
user    0m0.015s
sys     0m0.000s

$ time ./sum.tcl iis.log > c

real    0m15.031s
user    0m0.015s
sys     0m0.000s


# verify the output are the same
# btw, python and tcl treated the default end of line to be CRLF (native platform is Windows)
$ for i in a b c
do
dos2unix < a | md5sum
done
83211bf4faa32495ca9eb52c6b520974 *-
83211bf4faa32495ca9eb52c6b520974 *-
83211bf4faa32495ca9eb52c6b520974 *-

It is clear the both Python and Tcl come in neck to neck. A comprehesive scripting language like Python and Tcl is definitely more versatile than a specific tool such as AWK. Below are the source codes for the various programs in case you are interested in the details:

$ cat sum.py
#! /cygdrive/c/Python25/python

import sys

if len(sys.argv) != 2:
        print "Usage:", sys.argv[0], ""
        exit(1)


sc={}
cs={}
cnt={}
for i in range(24):
        index='%02d' % i
        sc[index]=0
        cs[index]=0
        cnt[index]=0


file=open(sys.argv[1],'r')
line=file.readline()
while line:
        fields=line.split()
        times=fields[1].split(':')
        hour=times[0]
        sc[hour] += int(fields[18])
        cs[hour] += int(fields[19])
        cnt[hour] += 1
        line=file.readline()
file.close()


k=sc.keys()
k.sort()
for i in k:
        print i,sc[i],cs[i],cnt[i]




$ cat sum.sh
#! /bin/sh

if [ $# -ne 1 ]; then
        echo "Usage: $0 <input-log>"
        exit 1
fi

awk '
{
        split($2,t,":")
        hr=t[1]
        sc[hr]+=$19
        cs[hr]+=$20
        hit[hr]++
}
END {
        for ( h=0 ; h<24 ; ++h ) {
                hh=sprintf("%02d",h)
                print hh, sc[hh], cs[hh], hit[hh]
        }
}' $1




$ cat sum.tcl
#! /cygdrive/c/ActiveTcl/8.4.12.0/bin/tclsh

if { $argc != 1 } {
        puts stderr "Usage: $argv0 "
        exit 1
}
set logfile [lindex $argv 0]
if { ![file exists $logfile] } {
        puts stderr "Error. $logfile does not exist"
        exit 2
}


# initialise to 0
set hours {}
for { set h 0 } { $h < 24 } { incr h } {
        lappend hours [format {%02d} $h]
}
foreach hr $hours {
        set sc($hr) 0
        set cs($hr) 0
        set hit($hr) 0
}


set fp [open $logfile r]
while { [gets $fp line] >= 0 } {
        set time [lindex $line 1]
        set hr [lindex [split $time :] 0]
        incr sc($hr) [lindex $line 18]
        incr cs($hr) [lindex $line 19]
        incr hit($hr)
}
close $fp


foreach hr $hours {
        puts "$hr $sc($hr) $cs($hr) $hit($hr)"
}

I just covered 200 pages (out of 746 pages) of the Learning Python, 3rd Edition and hope to explore more features as I go into the details. So far, I particularly like the feature-rich OO methods available in their core objects. However, I still have not figure out how to differentiate between attribute and method of an object.

Labels: , , ,

Saturday, May 31, 2008

AWK Can Do Lookup

It is possible to program in AWK to do direct lookup via an input file. All you have to do is to establish the associate array (in my case, I store them in array L) in the BEGIN block.

I choose the web access log as an example and the lookup is based on the Hypertext Transfer Protocol -- HTTP/1.1 Status Code Definitions, eg, 200 -> OK

My initial version is based on some shell tricks which are very inefficient and error-prone. After browsing through the "The AWK Programming Language" (written by the AWK author - Alfred V. Aho, Peter J. Weinberger, and Brian W. Kerninghan), I am able to come up with this clean and readable code. Although the book was written in 1988, IMHO it is still the best book for AWK

#! /bin/sh

if [ $# -ne 2 ]; then
 echo "Usage: $0 <lookup-file> <data-file>"
 exit 1
fi


if [ ! -f $1 ]; then
 echo "Error. \"$1\" lookup file does not exit"
 exit 2
fi
if [ ! -f $2 ]; then
 echo "Error. \"$2\" data file does not exit"
 exit 3
fi


gawk '
BEGIN {
 # establish lookup
 while ( getline < "'$1'" > 0 ) {
  V=$2
  for ( i=3 ; i<=NF ; ++i ) {
   V=sprintf("%s %s",V,$i)
  }
  L[$1]=V
 }

}
{
 # HTTP status code summary
 ++s[$9]
}
END {
 for ( i in s ) {
  printf("\"%s\" has %d counts\n", L[i], s[i])
 }
}' $2

See the lookup file and access log, and how the above script generates the lookup dynamically

$ cat lookup.txt
200 OK
201 Created
202 Accepted
203 Non Authoritative Information
204 No Content
205 Reset Content
206 Partial Content
300 Multiple Choices
301 Moved Permanently
302 Found
303 See Other
304 Not Modified
305 Use Proxy
306 Unused
307 Temporary Redirect
400 Bad Request
401 Unauthorized
402 Payment Required
403 Forbidden
404 Not Found
405 Method Not Allowed
406 Not Acceptable
407 Proxy Authentication Required
408 Request Timeout
409 Conflict
410 Gone
411 Length Required
412 Precondition Failed
413 Request Entity Too Large
414 Request URI Too Long
415 Unspported Media Type
416 Request Range Not Satisfiable
417 Expectation Failed
500 Internal Server Error
501 Not Implemented
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout
505 HTTP Version Not Supported

$ head access_log
127.0.0.1 - - [01/Mar/2006:15:30:26 +0800] "GET / HTTP/1.1" 200 1456
127.0.0.1 - - [01/Mar/2006:15:30:26 +0800] "GET /apache_pb.gif HTTP/1.1" 200 2326
127.0.0.1 - - [01/Mar/2006:15:30:30 +0800] "GET /manual/ HTTP/1.1" 200 9187
127.0.0.1 - - [01/Mar/2006:15:30:30 +0800] "GET /manual/images/pixel.gif HTTP/1.1" 200 61
127.0.0.1 - - [01/Mar/2006:15:30:30 +0800] "GET /manual/images/apache_header.gif HTTP/1.1" 200 4084
127.0.0.1 - - [01/Mar/2006:15:30:30 +0800] "GET /manual/images/index.gif HTTP/1.1" 200 1540
127.0.0.1 - - [01/Mar/2006:15:30:38 +0800] "GET /manual/howto/cgi.html HTTP/1.1" 200 22388
127.0.0.1 - - [01/Mar/2006:15:30:38 +0800] "GET /manual/images/home.gif HTTP/1.1" 200 1465
127.0.0.1 - - [01/Mar/2006:15:30:38 +0800] "GET /manual/images/sub.gif HTTP/1.1" 200 6083
127.0.0.1 - - [01/Mar/2006:15:33:15 +0800] "GET /manual/howto/cgi.html HTTP/1.1" 200 22388

$ ./lookup.sh lookup.txt access_log
"Not Modified" has 239 counts
"Bad Request" has 1 counts
"Unauthorized" has 18 counts
"Forbidden" has 23 counts
"OK" has 11378 counts
"Not Found" has 3257 counts
"Internal Server Error" has 4 counts
"Bad Gateway" has 2 counts

Labels: , ,