Monthly Archives: May 2009

Use lassign to Assign Command-Line Parameters to Variables

The Problem

You need to assign command-line parameters to variables. For example:

    set [lindex $argv 0] server
    set [lindex $argv 1] port
    set [lindex $argv 2] user
    set [lindex $argv 3] password

The problem with this approach is it takes lots of typing. Cut and paste can help reducing some of it, but it is error prone for the user might forget to edit the index numbers after pasting. When the number of command-line parameters increase, so is the chance for making error.

The Solution

One way to deal with assigning command-line parameters to variables is to use the lassign command from the Tclx package:

    package require Tclx
    lassign $argv server port user password

Not only this approach is cleaner and less error prone, it is also easier to understand. The user must make sure argv has enough elements to assign to the variables. If there is more variables than the number of command-line parameters, then the extra variables will be assigned the empty value {}:

    % lassign {1 2} a b c
    % puts "a=$a, b=$b, c=$c"
    a=1, b=2, c=

If there are more command-line parameters than variables, then lassign will return a list of unassigned parameters:

    % lassign {1 2 3 4 5} a b c
    4 5

Conclusion

The lassign command eases the task of assigning command-line parameters to variables. The user must exercise care to ensure the number of parameters matches the number of variables. Lassign can also be used for any list-variable assignment task the user can think of. Please take a look at the document for more usage and behavior notes.

Advertisements

Tcl: Use commandloop to Provide Interactive Access to Your Procedures

When I wrote text base programs in high-level programming languages, I often need to create a simple menu, get the user’s choice, then dispatch the appropriate procedure. The dispatch code often include lengthy swich (case) statement:

    switch (choice) {
	case 'a':
		function_a();
		break;
	case 'b':
		function_b();
		break;
	}
    ...

This design pattern was fine if the menu is relatively short. As the menu grows, adding a choice to the menu means: 1) Add lines to the menu, 2) update the switch statement, and 3) write another function to handle the new choice.

This design pattern bears several problems:

  1. Adding an additional choice to the menu requires a lot of works as illustrated
  2. Programmer must take care to keep the menu and the switch statement in sync.
  3. As the menu grows, the switch statement can get very long

Fortunately, Tcl offers an easy remedy to this problem: commandloop. I wrote about commandloop before as my way to setup a break point for debugging purpose. Commandloop can do more than that. In this case, I can commandloop to provide dispatch to my procedures. Consider the following simple script:

	package require Tclx

	set stack {}

	proc stack {} { set ::stack }
	proc push {item} { lappend ::stack $item }
	proc pop  {}     { 
		set item [lindex $::stack end]
		set ::stack [lreplace $::stack end end]
		return $item
	}

	commandloop

This script simulates a stack; it provides three commands (procedures): stack to display the contents of the stack and the self-explanatory push and pop. The script’s main body consists of a single commandloop command. So how does this works?

The commandloop command allow the users to interact with the script, including the ability to call the script’s commands. Let’s take a look at an interactive session:

    $ ./commandloop1.tcl 
    %stack
    %push 5
    5   
    %push 7
    5 7 
    %push -3
    5 7 -3
    %stack
    5 7 -3
    %pop
    -3  
    %stack
    5 7 
    %set x 0 
    %push $x
    5 7 0 
    %puts "Stack contents: [stack]"
    Stack contents: 5 7 0 
    %exit
    $   

After launching the script, we arrive at the ‘%’ prompt. From there, we can issue the script’s commands such as stack, push, and pop. Not only that, we can also set additional variables (such as x). We can all issue other commands such as puts. In short, the commandloop command opens up an interpreter to interact with the script. Finally, the exit command (or Ctrl-D in Linux, Ctrl-Z in Windows) exits the command loop and ends the script.

Commandloop comes with some handy optional flags. The first is the -endcommand flag. This flag specifies a command (or procedure) to execute after commandloop terminates. Note that after commandloop terminates, the script’s execution will stop, ignoring any command that follows. If you want to execute a command after the commandloop terminates, use this flag.

Another flag of interest is the -prompt1 flag. This flag allows the users to customize the prompt if they don’t like the default one. These are the two flags I use most often. For information regarding other flags, you can look up information for commandloop in the Tclx package.

Debugging Tcl Script with commandloop

Besides using puts to debug my Tcl script, I also like this technique, which set up a break point within my script. There are times when I cannot use that technique, I use the commandloop command to create an impromptu break point.

Before we get started, let me clarify that the purpose of commandloop is not for debugging, but that is what I am using it for. Consider the following block of code:

    # Given a number x, double it
    proc double {x} {
        upvar 1 x xValue
        set xValue [expr {$xValue * 2}]
    }

Those who knows Tcl can spot the bug right away, but for argument sake, let’s pretend that the bug was well hidden and I need to set a break point to debug the procedure double. I can accomplish this goal using the commandloop command:

    package require Tclx
	
    # Given a number x, double it
    proc double {x} {
        upvar 1 x xValue
        commandloop
        set xValue [expr {$xValue * 2}]
    }

During execution, commandloop will halt the code and display the ‘%’ prompt. At this point, I can execute any Tcl command to debug the problem:

    $ tclsh db.tcl 
    %set x
    n
    %set xValue
    Error: can't read "xValue": no such variable
    %exit

    $ 

In the capture above, the script stopped just after the upvar line and displayed the ‘%’ prompt. I then issue some Tcl commands to debug and finally was able to pinpoint the problem–the upvar line should have been:

    upvar 1 $x xValue

One thing to note, unlike the aforementioned solution, commandloop has one disadvantage: upon typing exit, the script will exit right away, ignoring the rest of the script. However, in a crunch, this command provides a quick way to set break point to debug my script.

Use for_file to Ease Line-by-Line Processing

In my job, I often need to open a file, read and process each line until the end. In Tcl, that pattern can be translated as:

	set infile [open file.txt r]
	while {[gets $infile line] >= 0} {
		# do something with $line...
	}
	close $infile

Simple? Yes, but I can still see room for improvements. The Tclx package has a for_file command which can simplify the coding quite a bit:

	package require Tclx
	for_file line file.txt {
		# do something with $line...
	}

Not only I don’t have to worry about openning and closing the file, I don’t have to deal with the lengthy while/gets command, which can be error-prone. Finally, the second construct is much cleaner and easier to understand.

After learning about the for_file command, I discovered that the fileutil package also has a similar command:

	package require fileutil
	fileutil::foreachLine line file.txt {
		# do something with $line...
	}

It seems either one of them will get the job done. Personally, I prefer the for_file command because it is shorter. If you are aware of any differences between the two, please comment.

Display Weather from Command Line

Since I work on Linux/OSX command line all day, I prefer to get my information such as stock quote or weather via command line. Here is a script to do that.

A couple of notes:

  1. I use curl instead of the TclCurl package because my system does not come with TclCurl
  2. The script employs Google Weather API to retrieve weather data. The rest of the script deals with converting that data from XML format to human-readable format.
  3. I use this script as part of my geek tool output.

Here is a sample output of the script:

$ weather
Seattle, WA 98121 2009-05-08 22:15:09 +0000 
  Condition      : Overcast
  Temperature (F): 61
  Humidity       : 47%
  Wind           : NE at 2 mph

  Fri 63 45 Mostly Sunny 
  Sat 68 47 Mostly Sunny 
  Sun 67 49 Mostly Sunny 
  Mon 61 47 Chance of Showers 

Bothell, WA 98012 2009-05-08 22:18:51 +0000 
  Condition      : Cloudy
  Temperature (F): 55
  Humidity       : 58%
  Wind           : N at 2 mph

  Fri 59 41 Mostly Sunny 
  Sat 63 43 Mostly Sunny 
  Sun 65 45 Mostly Sunny 
  Mon 58 41 Chance of Showers 

Here is the script itself:

#!/usr/bin/env tclsh

package require tdom

# Provide the default zip codes of none specified
if {$argc == 0} { set argv {98121 98012} }

foreach location $argv {
    set xml [exec curl --silent http://www.google.com/ig/api?weather=$location]

    set doc [dom parse $xml]
    set root [$doc documentElement]

    #
    # Show Forecast Information
    #

    set inf [$doc getElementsByTagName forecast_information]
    set data ""
    append data "[[$inf selectNodes city] getAttribute data] "
    append data "[[$inf selectNodes postal_code] getAttribute data] "
    append data "[[$inf selectNodes current_date_time] getAttribute data] "
    puts "$data"


    #
    # Show Current Condition
    #
    set currentConditions [$doc getElementsByTagName current_conditions]

    set labels {
        condition      "Condition"
        temp_f         "Temperature (F)"
        humidity       "Humidity"
        wind_condition "Wind"
    }

    set longest 0
    foreach {tag labl} $labels {
        set length [string length $labl]
        if {$length > $longest} { set longest $length }
    }

    foreach {tag labl} $labels {
        set node [$currentConditions selectNodes $tag]
        set data [$node getAttribute data]
        regsub {[^:]*: *} $data "" data
        puts [format "  %-*s: %s" $longest $labl $data]
    }

    puts ""

    # Show Conditions for Next Days
    foreach cond [$doc getElementsByTagName forecast_conditions] {
        set data ""
        append data "[[$cond selectNodes day_of_week] getAttribute data] "
        append data "[[$cond selectNodes high] getAttribute data] "
        append data "[[$cond selectNodes low] getAttribute data] "
        append data "[[$cond selectNodes condition] getAttribute data] "
        puts "  $data"
    }
    puts ""
}


View Stock Price Using Command Line

I live on command line all day, so it is convenience to perform many tasks using the command line. One of those is to check stock price:

curl -s 'http://download.finance.yahoo.com/d/quotes.csv?s=csco&f=l1'

The above command check for the last price (with some delay) of Cisco Systems. If you want to check the price for other tickers, replace ‘csco’ with your ticker symbol. You can query more than one symbols by separate them with commas, but do not leave any space in between.

The f=l1 defines the formatting for the returned information. To find more about formatting, visit this page

Please note this trick requires curl so install it before you try.

Locate a File and Go to It

I often need to find a file, then cd to its directory. This command will do both, provide that the file’s name is unique enough:

$ cd $(dirname $(find ~ -name emails.txt))

Please note that the above command works for bash shell, but not csh: csh’s equivalent to the $( … ) construct is the ` … ` construct which the bash shell also understands. However, the “ construct does not allow for nesting.