Saturday, December 13, 2008

Learn to Talk AWK

When it comes to slicing and dicing text, few tools are as powerful, or as underutilized, as awk. The name AWK was coined from the initials of it's authors, Aho, Weinberger, and Kernighan. Yes, the same Kernighan of the famous K&R C Programming Language book. In the Linux world, every distribution includes the GNU version, gawk, (/bin/awk is usually a symbolic link to /bin/gawk). OS X ships with the BSD version of awk, more closely related to the original UNIX awk. The focus of the article is on the core features common among POSIX-compliant awks.

note: originally published January 16, 2006 on linux.com
updated for OS X on October 2, 2007

Basic command line usage


The awk utility is a small program that executes awk language scripts, often one-liners, but just as adeptly larger programs saved in a text file. For example, to execute an awk script saved in the file prg1.awk, and have it process the file, data1, you could use this command:

awk -f prg1.awk data1

The result is written to standard out, so it is usually piped to a result file.

Another command line parameter commonly used is -F to change the default field separator of a blank space. The field separator can also be changed within an awk program. To tell awk how to split data into fields from a comma separated variable (CSV) file, you would use:

awk -F"," -f prg1.awk data1

You may also include more than one data file to process and awk will keep running until it runs out of data:

awk -F"," -f prg1.awk data1 data2 data3 data4 data5

If you want to assign a value to a variable before execution of the program, use the -v option:

awk -v AMOUNT=100 prg1.awk data1

Behold the power

The power of awk comes from how much it does automatically for you when crunching text files, and from the simple elegance of the language. When you feed awk a text file, it does the following things for you:

  1. Opens and reads all input files listed on the command line
  2. Handles memory management for all variables
  3. Parses each line and splits it into fields using the field separator (the default is a blank space, but can be changed)
  4. Presents each line of text to your program as variable $0
  5. Presents each field from each line in predefined variables, starting with $1, $2, ... $N
  6. Maintains many internal variables for your use such as (but not limited to ):
    • RS = record separator
    • FS = field separator
    • NF = number of fields in the current record
    • NR = number of records processed so far
  7. Automatically handles conversion between internal data types (string, floating point, array)
  8. Executes the BEGIN block before processing any records (a good place to initialize variables)
  9. Executes the END block after processing all records (a good place to calculate report totals)
  10. Closes all input files listed on the command line

The awk language uses only three internal data types: strings, floating point numbers, and arrays. Variables do not have to be defined before they are used. Awk handles converting data from one type to another as necessary. If you add two strings together using the addition operator (+) and they contain numeric values, you get a numeric result. If a string is used in an arithmetic operation but can't be converted to a number, it is converted to zero. Usually, awk does what you want when handling data conversion.

It can open, read, and write to more files than those listed on the command line by using the getline function or redirecting output from within a program. It has access to a set of internal functions that include math, string manipulation, formatted printing (similar to the C language printf), and miscellaneous functions like pseudo-random numbers. You can also create your own functions or function libraries that can be used in several programs. All of this is packed into an executable usually about 500k in size. Programmers can typically become proficient in awk within a day. Complete references are available in a single book. You don't need a "bookshelf" of dead trees and CDs to master awk.

Implementations or ports of awk are available on nearly every platform, making your scripts reasonably portable.

AWK in the real world

Here is a short example of an awk application I created to import a list of email addresses and names from Novell Groupwise to PHPList (a mailing list manager). The list was exported from Groupwise in vCard File format (VCF), a text based format. Here is an example entry from the VCF file:

BEGIN:VCARD
VERSION:2.1
X-GWTYPE:USER
FN:Bar, Foo
ORG:;GREEN
EMAIL;WORK;PREF:foobar@yahoo.com
N:Bar;Foo
X-GWUSERID:foobar
X-GWADDRFMT:0
X-GWIDOMAIN:yahoo.com
X-GWTARGET:TO
END:VCARD

The target format was a CSV file that PHPList could import into an existing mailing list. I needed to extract the name (from the record that starts with "FN" and the email address from the record that starts with "EMAIL". These records are easy to identify and a small awk script does the job nicely.

I started construction of the script by setting up a custom record separator and a block of code to handle each record type. I saved the script in a text file called extract-emails.awk. Note that the .awk file extension is just convention, the file containing awk commands can be named anything. This was the beginning of the script:

BEGIN { FS = ":" }

/^FN/ {
# handle name here
}

/^EMAIL/ {
# handle email address here
}

The BEGIN block is run once before any records are read. It sets the field separator to a colon so awk will split the fields of the file when a colon is encountered.

The regular expressions /^FN/ and /^EMAIL/ tell awk to look for the characters "FN" or "EMAIL" at the start of a record, and if a match is found, run the associated block of code between the curly braces. This kind of regular expression match is common in awk but not required. A block of code with no match expression is run for every record processed by awk. I added a couple of comments (lines starting with "#") to document what each part of the script does.

Looking at the VCF data, I noticed that the "FN" record always precedes the "EMAIL" record, so I ordered the code blocks to process the records that way. Awk reads and executes a script in the order it appears. Many times, the order of the code will not matter, but in this case it does. The name is related to the email and I need to retain that relationship as the file is read, so I saved the name in an internal variable, then wrote both the email address and name to standard out while processing the email record.

Getting back to the task, let's complete the name section. The goal is to reformat the name from "lastname, firstname" into "firstname lastname", removing the comma. Here was my solution for the name:

/^FN/ {
# handle name here
fullname = tolower($2)
split(fullname, names, ",")
name = names[2] names[1]
}

Knowing that awk has split up the incoming records into fields using a colon as the field separator, the field variables for the example "FN" record contain the following:

$1 = "FN"
$2 = "Bar, Foo"

Working with the $2 variable, I used a built in awk function, towlower(), to convert the names to lowercase and stored the result in a variable called "fullname". Next, I used the split function to break the name into first and last name parts, with the result stored in an array called "names". Finally, I glued the name back together in the desired order, without the comma, and stored that result in a variable called "name".

There is very little to do inside the email code block. Awk provides the email address to us in the $2 variable (note that $2 in the "EMAIL" record is different than $2 in the "FN" record). For consistency, I converted it to lowercase, then used the print function to write both the email address and name to standard out, with a comma separating the values. Here is the complete script:

BEGIN { FS = ":" }

/^FN/ {
# handle name here
fullname = tolower($2)
split(fullname, names, ",")
name = names[2] names[1]
}

/^EMAIL/ {
# handle email address here
mail = tolower($2)
print mail "," name
}

A sprinkle of shell glue

To pull it all together, we need a little shell glue. A small shell script allows us to call awk with the command line parameters we want and to easily redirect the output to a file. It is also handy to run a shell script when you are testing.

#!/bin/sh
# Extract e-mail addresses from VCF file for PHPList.
awk -f extract-emails.awk groupwise.vcf > phplist-emails.txt

Awk can be used as an intermediate step in a larger shell script where the output is fed into another utility such as sort, grep, or another awk script.

Finally, here is a sample of the output:

foobar@yahoo.com, foo bar
barbaz@yahoo.com, bar baz

Where AWK falls short

There are certain tasks that are beyond the capabilities of awk. For instance, if you need to do anything that communicates using network sockets, awk is not your best bet. Similarly, if you need to process binary files, awk falls short. The latest version of GNU awk does have some rudimentary network capabilities, but Perl, PHP, and Ruby are much better equipped for those tasks.

A million household uses

Awk is an expert tool for text processing, and the roots of Perl are clear in it's design. It is powerful enough to handle almost any kind of text crunching or reporting, while being very easy to learn and use. There is a lot of competition and many choices when it comes to scripting languages, but I still find awk the best choice for many problems. Although awk is employed most often for smaller problems, it can be used for large applications. I worked on a 12,000 line awk application used to adjudicate dental claims. This application was the core system for a successful million dollar business. Despite being a common utility on every Linux system, awk remains relatively obscure. If you take the time to learn it, the rewards will last a lifetime.

Cron fields and crontabs

Crontab fields

Here is a typical crontab entry and the definition of cron fields:
00 03 * * * /tmp/zing.sh

  1. minute
  2. hour
  3. day of the month
  4. month of the year
  5. day of the week
  6. program or command to run

An asterisk (*) in any field means run every time for this field.
Ranges (X-Y) and steps (X-Y/Z) can also be defined.



User crontabs (including the root user)

To edit a user crontab (including root):
crontab -e

To delete a crontab:
crontab -r

System crontab

The system crontab is stored in /etc/crontab. It can be changed (as root) with a text editor.

The system crontab has one extra field before the program to run, which is the user to run the command as (usually root).

Periodic jobs in OS X Tiger

The system cron jobs were replaced in Tiger with launch daemons, in /System/Library/LaunchDaemons/. There are daily, weekly, and monthly launch daemons for system maintenance. They run shell scripts in /etc/periodic/.

The system shell scripts will run user scripts on the same schedule if they exist. To take advantage of this, simply create your own local scripts called /etc/daily.local, /etc/weekly.local, or /etc/monthly.local. They are run as root.

Spotlight on the command line

In addition to the traditional command line search programs, locate and find, OS X provides a command line interface to the Spotlight search system (mdfind).

While I can't find it documented anywhere, my guess is that "mdfind" stands for "meta data find".



Simple spotlight search

mdfind searchterm

Search in a specific directory

mdfind -onlyin /Users/keithw searchterm

Search for specific meta data

mdfind "kMDItemAuthor == '*Donaldson*'"

List available meta data fields

mdls filename

Turn off spotlight indexing for a volume

mdutil -i off volumename

Turn on spotlight indexing for a volume

mdutil -i on volumename

Show the status of spotlight indexing for a volume

mdutil -s volumename

How spotlight does its magic

Unlike the locate program and database, spotlight recognizes and indexes file system changes almost immediately. How is that possible? The magic happens in the kernel, which sends notices of file system changes to a special character device named /dev/fsevents. Spotlight monitors this special device and learns about new files that need to be indexed. There is no published API for /dev/fsevents, as far as I can tell.

Surfing the Keyboard in BASH

You are probably familiar with common keyboard sequences like Ctrl-C to end a program, but there are dozens of useful shortcuts you can use in BASH to edit the command line, move around your command history, and control jobs. I've collected 18 useful Ctrl and Meta (Option) sequences to make you more productive in BASH. So grab your 'board and leave the wet suit at home, the water is warm this time of year.

Command Line Editing

BASH keyboard sequences come in two flavors, Ctrl and Meta. Ctrl sequences are straightforward, you hold down the Ctrl key while pressing another key. On a PC with Linux, the Meta key is usually mapped to Alt, but on a Mac, it is usually mapped to the Esc key. Since the Esc key is poorly placed for serious keyboarding, I suggest mapping the Meta key to the Option key.

To map Meta to Option in the OS X Terminal, use the menu and go to Terminal / Window Settings, select Keyboard in the drop down box at the top, and check 'Use option key as meta key'.

To map Meta to Option in iTerm, use the menu and go to Bookmarks / Manage Profile, expand Keyboard Profiles, select Global, and check the 'Option Key as Meta' radio button.

With that out of the way, here are some shortcuts to edit commands.

Key Sequence Result
Ctrl-A moves the cursor to the beginning of the line
Ctrl-E moves the cursor to the end of the line
Ctrl-D deletes the character under the cursor
Ctrl-F moves the cursor forward one character
Ctrl-B moves the cursor backward one character
Ctrl-K deletes from the cursor to the end of the line
Option-B moves the cursor back one word
Option-F moves the cursor forward one word
Option-L downcase a word starting from the cursor
Option-U upcase a word starting from the cursor

Moving through history

BASH records all commands you enter in a personal history file called .bash_history in your home directory. Notice the dot (.) at the start of the name, indicating it is a hidden file. Finder won't show you hidden files without some tweaking, but you can easily see hidden files in Terminal with the ls -a command. If you are paranoid, BASH has options to turn history off, but it is very useful if you work much at the command line. Here are the history shortcuts:

Key Sequence Result
Ctrl-R start an incremental reverse history search, Ctrl-J to stop
Ctrl-S start an incremental forward history search, Ctrl-J to stop
Ctrl-J stop incremental search, leaving command to edit
Ctrl-P (also up arrow) fetch the previous command from history
Ctrl-N (also down arrow) fetch the next command from history

Incremental searches work by matching the string entered with the most recent history command that contains the string. The more you type, the more accurate the match. Use Ctrl-J (or the left or right arrow keys) to end the search, leaving the matched command on the command line ready to either edit or run.

Job Control

OK, nothing new here...

Key Sequence Result
Ctrl-Z suspends the foreground job, returns control to user
Ctrl-C send signal 3 (SIGQUIT) to the foreground job

Miscellaneous

Key Sequence Result
Ctrl-X Ctrl-V show current version of BASH

All examples given above are default key bindings in BASH. There are additional BASH functions that are not automatically bound to a key sequence. To see all current key bindings, plus all unmapped functions, run this command:

bind -P

If you want to set your own key sequences or macros, you can define them in a readline run control file in your home directory called .inputrc (you will need to create one). Readline is a library that provides command line control for BASH. To define a new key sequence, edit the .inputrc file and enter your new definitions one per line. You need to use the readline syntax as follows:

key-sequence: function

\C- means Ctrl plus another key
\M- means Meta (Option) plus another key
\e is the ESC character
\\ is a backslash
\" is a double quote
\' is a single quote

For example, to bind Option-z (meta-z) to the tty-status function, add this line to your .inputrc file:

"\M-z": tty-status

Close Terminal and start a new shell so it reads the .inputrc file, then try Option-z and you should see something like this:

load: 0.61 cmd: bash 10561 running 0.00u 0.00s

You can create your own key sequences for anything BASH supports, or change them to taste. Few people have a burning desire to change the default BASH key sequences, but if you need to change your keyboard surfing style, you know how... dude.

PostgreSQL

PostgreSQL is a robust and powerful open source database. It has more advanced features than any other open source database and scales well with huge datasets and high traffic loads.

By default, PostgreSQL listens on TCP port 5432.

It is not installed by default in OS X.

Dump all databases

pg_dumpall --clean > databases.sql

Dump a database with compression (-Fc)

pg_dump -Fc --file=database.sql --clean database

Dump a database, plain text, one schema only (-n)
pg_dump -Fp --file=filename.sql -n schema --clean database

Dump a single table

Specify the schema with the table name (if applicable) with pg_dump [-d database] -t schema.table

Dump a table definition (no data)

pg_dump -s [-d database] -t schema.table

Restore a database from a dump file

pg_restore -Fc database.sql

Restore a single table from a dump file

pg_restore -v -e -Ft -d database -t tablename dumpfile.tar
note: in this case, the dump file is in tar format, the database to restore to is after the -d switch and the table to restore is after the -t switch.

Copy data from a file into a table (from the psql client)

COPY table-name FROM '/path/to/filename' DELIMITER 'delimiter';

note: the file must be readable by postgresql (make it 755),

the default delimiter is tab.

Copy data from a table to a file (from the psql client)

COPY table-name TO '/path/to/filename' DELIMITER 'delimiter';

note: the directory and file must be writable by postgresql,

the default delimiter is tab




Start the PostgreSQL interactive terminal


psql

Psql - show a list of databases

\l
Lowercase L, not the number 1

Psql - show all users

select * from pg_user;

Psql - show all tables (including system tables)

select * from pg_tables;

Psql - show tables in the current context (database/schema)

\d

Psql - show description of tablename

\d tablename

Psql - show description of tablename, along with constraints, rules, and triggers

\d+ tablename

Psql - change current database

\c database;

Psql - show all schemas in the current database

\dn

Psql - Grant permissions on a schema to a user

GRANT ALL ON myschema TO user;

Psql - quit psql

\q

Psql - show help

\?

Psql - copy a table to a tab delimeted file

COPY table TO 'table.txt';

Psql - load a table from a tab delimeted file

COPY table FROM 'table.txt';

Psql - show permissions on database objects

\z [object]
r -- SELECT ("read")
w -- UPDATE ("write")
a -- INSERT ("append")
d -- DELETE
R -- RULE
x -- REFERENCES (foreign keys)
t -- TRIGGER
X -- EXECUTE
U -- USAGE
C -- CREATE
T -- TEMPORARY
arwdRxt -- ALL PRIVILEGES (for tables)
* -- grant option for preceding privilege
/yyyy -- user who granted this privilege

Psql - getting or setting sequence values
Get current value of a sequence:
SELECT currval('this_id_seq');

Set current value of a sequence to 1000:
SELECT setval('this_id_seq', 1000);


Run the vacuum utility


vacuumdb --verbose --analyze --all
Note: vacuum reclaims space from deleted records and updates indexes. It should be set up in cron. Newer versions of postgresql may run vacuum automatically.

Increase perfomance with shared memory

One effective performance tuning tip for Postgresql is to increase the shared memory buffers. This might require adding RAM to the server. Many Linux distros default to 32MB of shared memory, controlled by two kernel parameters:
/proc/sys/kernel/shmmax
/proc/sys/kernel/shmall

These values can be changed at run time, but it is better to set them at boot using the /etc/sysctl.conf file. This increases shared memory to 1GB:
# increase shared buffers for postgres at boot
kernel.shmmax=1073741824
kernel.shmall=2097152


Then, tell PostgreSQL to use 768MB of the 1GB available in the /var/lib/pgsql/data/postgresql.conf file:
shared_buffers = 98304 # min 16, at least max_connections*2, 8KB each


Restart PostgreSQL for the change to take effect.

MySQL

MySQL communicates through either local unix sockets or over TCP/IP port 3306 (default). Database names, tables, field names, and passwords are case sensitive. SQL Commands are not case sensitive.

The main configuration file is /etc/my.cnf. Usually doesn't need tweaking, except when using the InnoDB storage engine, or if you have high performance requirements.

The main command line utilities are mysql, mysqldump, and mysqladmin. Many people like the phpMyAdmin package to manage MySQL through a web browser.

MySQL is not part of the default install in OS X.



Server Administration

Show all running MySQL processes
mysqladmin --user=root --password=xxx processlist

Show detailed status report
mysqladmin --user=root --password=xxx extended-status

Reload grant tables (after making security table changes)
mysqladmin --user=root --password=xxx reload

Show running configuration settings
mysqladmin --user=root --password=xxx variables

Kill a slow or locked process
First, get the process id using processlist, then
mysqladmin --user=root --password=xxx kill id

Reset the value of an autoincrement field in a table
  1. mysql --user=root database
  2. alter table tablename autoincrement=100;
Note: the above resets the autoincrement field to 100. Use caution!

Alter a table so it can grow beyond 4 GB in size

Even if your OS supports file sizes greater than 4 GB, and even though MySQL supports tables larger than 4 GB, it will still give you a "table full" error message if you try to insert records into a table that has reached 4 GB unless you tell it to allow tables to grow larger. You can set a global option in my.cnf or you can tell MySQL on a table by table basis. This command will allow a table to grow to roughly 300 GB:

ALTER TABLE tbl_name MAX_ROWS=1000000000 AVG_ROW_LENGTH=300;


Security


Change/set the root password
  1. mysql --user=root mysql (initially no password)
  2. update user set Password=password('new_password') where user='root';
  3. flush privileges;
Create a user with remote update authority
  1. mysql --user=root --password=xxx mysql
  2. insert into user (Host, User, Password, Select_priv, Insert_priv, Update_priv, Delete_priv) values ('%', 'remote', password('xxx'), 'Y', 'Y', 'Y', 'Y');
  3. flush privileges;

Create a user with access to just the db1 database

  1. mysql --user=root --password=xxx mysql
  2. insert into user (Host, User, Password)
    values ('localhost', 'foo', password('xxx'));
  3. insert into db (Host, Db, User, Select_priv, Insert_priv,
    Update_priv, Delete_priv) values ('localhost', 'db1', 'foo',
    'Y', 'Y', 'Y', 'Y');
  4. flush privileges;

Backup and Restore


Dump all databases (schema and data)
mysqldump --user=root --password=xxx --all-databases > databases.sql

Dump a single database (schema and data)
mysqldump --user=root --password=xxx --databases db1 > db1.sql

Dump a single database (schema only)
mysqldump --all --no-data --user=root --password=xxx --databases db1 > db1.sql

Restore a database from a dump file
mysql --user=root --password=xxx <>

Load data from a text file

The LOAD DATA INFILE statement reads rows from a text file into a table at a very high speed.

For security reasons, when reading text files located on the server, the files must either reside in the database directory or be readable by all. Also, to use LOAD DATA INFILE on server files, you must have the FILE privilege on the server host. You can also load datafiles by using the mysqlimport utility; it operates by sending a LOAD DATA INFILE command to the server.

The defaults cause LOAD DATA INFILE to act as follows when reading input:

  • Look for line boundaries at newlines.
  • Do not skip over any line prefix.
  • Break lines into fields at tabs.
  • Do not expect fields to be enclosed within any quoting characters.
  • Interpret occurrences of tab, newline, or .. preceded by .. as literal characters that are part of field values.

Example using the defaults (tab delimited):
LOAD DATA INFILE "data.txt" INTO TABLE table;

Example using an unquoted CSV file:
LOAD DATA INFILE "data.txt" INTO TABLE table FIELDS TERMINATED BY ',';

Tuning


Log slow queries

MySQL can log queries that take a long time to complete by adding this option to /etc/my.cnf in the [mysqld] section:
log-slow-queries=/var/log/mysqld.slowquery.log

The file should be created first with an owner of mysql:mysql. By default, a slow query is one that takes more than 10 seconds.

Thursday, December 11, 2008

Gnu Privacy Guard (GPG)

The Gnu Privacy Guard (GPG) is a command line application that can use public key encryption to safeguard text and data and prepare it for transport through the email system.

It is not part of the default install of OS X and must be installed manually.

To generate a new key pair:
gpg --gen-key

To list keys on your public keyring:
gpg --list-keys

To list keys on your secret keyring:
gpg --list-secret-keys

To encrypt a text file "message.txt" for recipient "foo" with ASCII armor (Base64):
gpg -e -a -r foo message.txt
The encrypted message is saved as file "message.txt.asc".

To encrypt a text file "message.txt" for recipient "foo" with ASCII armor and sign it with your secret key:
gpg -s -e -a -r foo message.txt

To import a public key:
gpg --import keyfile

To sign a newly imported key with your secret key:
gpg --sign-key keyname

To delete a public key from the keyring:
gpg --delete-key keyname

To verify a file with a detached signature:
gpg --verify signature data-file

To import a public key:
gpg --import keyfile

Set up a trusted public key (no passphrase required):

If you want to encrypt files in a script and not be prompted for your passphrase, you need to sign all public keys you want to use. Follow this procedure (GPG 1.2.5+) to sign a public key.

  1. gpg --edit-key keyname
  2. at the prompt, enter "trust"
  3. select "4" for trust fully
  4. enter "lsign" to locally sign it
  5. at the prompt, enter 3 for very careful checking
  6. answer "yes" to the the "Really Sign?" prompt
  7. enter secret key passphrase when requested
  8. enter "save"
The key is now signed and can be used in a script without passphrase requirements.