Chapter 5. Working with Lists

Lists are one of Tcl’s two native or built-in data structures (the second is associative arrays, treated in Chapter 6, “Creating and Using Arrays”). Tcl has a broad set of commands for dealing with lists, and this chapter will get you up to speed with them. I’ll finish up the discussion of control structures by introducing the switch command, another command used for conditional execution, and the foreach command, a looping control structure that specializes in iterating over list items. The chapter ends with the two commands you can use to interrupt loop execution, break and continue.

Playing Blackjack

This chapter’s game is a Spartan Blackjack implementation. To play it, execute the script blackjack.tcl in this chapter’s code directory.

$ ./blackjack.tcl
Queen of Clubs
Ace of Hearts
Deal another card [yn]? n
21: Perfect!
$ ./blackjack.tcl
6 of Spades
8 of Spades
Deal another card [yn]? n
14: Better luck next time!
$ ./blackjack.tcl
9 of Hearts
4 of Spades
Deal another card [yn]? y
9 of Hearts
4 of Spades
4 of Clubs
Deal another card [yn]? n
17: Tough hand to beat!

What Is a Tcl List?

A Tcl list is nothing more than an ordered sequence of (potentially heterogeneous) values, separated by a space character. I’ll use the word element to refer to the individual list items. Elements that contain embedded white space need to be grouped (yep, more grouping—I told you that grouping is a pervasive element of Tcl) using braces or quotes. Like strings, lists are accessed by their indices, which are 0-based.

The most cogent example of Tcl lists is a Tcl command. In fact, Tcl lists have the same syntax as Tcl commands. A command is a list, the first element of which is the command itself, and the remaining elements of which are the arguments to that command. Not surprisingly, then, the same rules and considerations that apply to creating Tcl commands (think grouping and substitution here) also apply to creating Tcl lists.

Let me explain my definition of a Tcl list as an ordered sequence of heterogeneous values. Ordered in this context doesn’t necessarily mean that lists are sorted. Rather, lists are ordered because list elements are accessed by their indices, that is, by their position in the list and because list access is idempotent. Each time you access list index N, which must be an integer, you get the same element. For example, given the list of fruits {orange apple pineapple}, element 0 is always orange, element 1 is always apple, and element 2 is always pineapple (provided, of course, you don’t change the order).

Tcl lists also consist of potentially heterogeneous elements, which is just a fancy way to say that lists can have elements of mixed data types. Unlike arrays in traditional programming languages (like C), which consist of homogeneous elements such as integers or strings, Tcl lists can contain integers, strings, characters, and even other lists. For example, {a b 893 "Some random string" {Z Y X W}} is a perfectly valid list consisting of five elements: the character a, the character b, the number 893, the string "Some random string", and the embedded list {Z Y X W}.

Note: All Arrays Are Not Created Equal

Note: All Arrays Are Not Created Equal

In older programming languages, arrays do, in fact, consist of elements that all have the same data type. However, many of the newer programming and scripting languages, including Tcl, use associative arrays, in which the array members have both a name and a value and are accessed by their name rather than their index position. You’ll learn to use arrays in Chapter 6.

Creating Lists

How do you create a list in Tcl? The easiest and by far the most efficient way is to use the list command:

list item1 [item2] ...

The list command creates and returns a list which consists of the arguments item1, item2, and so forth. Each argument to list must be separated by whitespace. An interesting and useful side effect of this syntax is that when you use list to create a list, the whitespace delimiters between elements make it trivial to resolve quoting and grouping issues—each white-space-delimited argument becomes a single element of the list. The following example shows how to create lists. I used tclsh in this example to show how the list command automatically handles quoting (see list.tcl in this chapter’s code directory):

$ tclsh
% set faceCards [list Ace King Queen Jack]
Ace King Queen Jack
% set acesBySuit [list {Ace of Hearts} {Ace of Diamonds} {Ace of Spades} {Ace of Clubs}]
{Ace of Hearts} {Ace of Diamonds} {Ace of Spades} {Ace of Clubs}
% set winningHands [list "Royal Flush" "Flush" {Straight Flush} {Full House} "Three of a Kind" {Two of a Kind} "Two Pair"]
{Royal Flush} Flush {Straight Flush} {Full House} {Three of a Kind} {Two of a Kind} {Two Pair}
% set junk [list $faceCards $acesBySuit $winningHands]
{Ace King Queen Jack} {{Ace of Hearts} {Ace of Diamonds} {Ace of Spades} {Ace of Clubs}}
{{Royal Flush} Flush {Straight Flush} {Full House} {Three of a Kind} {Two of a Kind}
{Two Pair}}

In the first example, I assign faceCards a list consisting of four elements: Ace, King, Queen, and Jack. In the second example, I create another four-element list, acesBySuit. This time, though, because the elements contain spaces, I use braces to group the items that make up each element. The third list, winningHands, uses both single- and multi- word elements and the multi-word elements are grouped with both braces and double quotes. The feature to notice with winningHands is that list handles the quoting automatically—items that were grouped with double quotes are displayed with brace grouping.

The fourth list, junk, is a list of lists. It consists of three elements, {Ace King Queen Jack}, {{Ace of Hearts} {Ace of Diamonds} {Ace of Spades} {Ace of Clubs}}, and {{Royal Flush} Flush {Straight Flush} {Full House} {Three of a Kind} {Two of a Kind} {Two Pair}}. Of course, each element is a list of its own.

Appending Lists

If you need to add items to the end of an existing list, use the lappend command. Unlike the list command (and most of Tcl’s list-related commands), lappend operates on an existing list instead of returning a new list. Its syntax is:

lappend listVar item1 ...

The first argument to lappend is the name of a list variable (listVar in the syntax diagram). The second and subsequent arguments are the items to append to the list.

Tip: lappend Also Creates a List

Tip: lappend Also Creates a List

If the variable name passed to lappend doesn’t exist, lappend creates it with the value specified. If no values are specified, lappend creates the variable with a null list. Because you can use lappend to create a new list, as well as modify an existing one, I’ve covered it in this section rather than in the section “Modifying Lists” later in the chapter.

The next section includes an example of using lappend.

Merging Lists

One drawback to lappend is that it maintains the list structure, if any, of appended elements that happen to be lists. In many cases, this is the behavior you want, but not always. The concat command works much like lappend, except that it does not maintain the list structure of appended elements. In addition, concat strips leading and trailing spaces from each of its arguments before concatenating them together. Before I explain, here is concat’s syntax:

concat ?item1 ...?

concat’s return value is the concatenated list with one level of list structure removed. To see the difference between lappend and concat, consider the following code, concat.tcl, in this chapter’s code directory:

set faceCards [list Ace King Queen Jack]
set numberedCards [list 10 9 8 7 6 5 4 3 2]

# lappend creates a list of two lists
lappend suit $faceCards $numberedCards
puts "$suit (length: [llength $suit])"

# concat creates a singe list
set suit [concat $faceCards $numberedCards]
puts "$suit (length: [llength $suit])"

You can see the difference in the resulting output:

$ ./concat.tcl
{Ace King Queen Jack} {10 9 8 7 6 5 4 3 2} (length: 2)
Ace King Queen Jack 10 9 8 7 6 5 4 3 2 (length: 13)

I discuss the llength command in the next section, but I bet you can guess what it does. The lappend command creates a list that consists of two elements: the list of face cards and the list of numbered cards; the list structure of the parent lists has been preserved. The concat command returns a list that consists of 13 elements, having created a single list by splicing together the two constituent lists and removing a single (and, in this case, the only) level of list structure from the parent lists. Neither result is correct or incorrect on its face, as it were. Rather, the correctness depends on the goal you are trying to achieve. If you need a simple list that contains all the elements of the parents, use concat. If you need to maintain list structure, use lappend.

Accessing List Elements

So, you’ve created a wonderful list. What can you do with it? It would probably be nice to know how many elements it contains or what element or elements are present at a given index or indices. llength tells you how many elements are in a list, as you saw in the example in the previous section; lindex returns the element at a given index in the list; and lrange returns the elements between and including a starting and ending index value. Their syntax is:

llength listVar
lindex listVar ?index ...?
lrange listVar start end

In each case, listVar is the list in which you are interested.

Accessing Specific List Elements

You can use lindex and lrange to retrieve one or more elements from a list. If you use lindex, the typical behavior is to specify an index value to retrieve. However, as you can see from its syntax diagram, the index value to fetch is optional. If you omit index, lindex returns the value of the list. Otherwise, lindex returns the element that corresponds to the specified index. For example, consider the following script:

set faceCards [list Ace King Queen Jack]
puts [lindex $faceCards 1]
puts [lindex $faceCards]

Executed (see lindex.tcl in this chapter’s code directory), the output of this script should be:

$ ./lindex.tcl
King
Ace King Queen Jack

So, lindex makes it possible to retrieve either a single list element or the entire list. If you request an index greater than the number of elements in the list or less than zero, lindex returns an empty string.

If you specify multiple indices, each index except for the last one returns a sublist of listVar. The last index value is the one that returns an actual list element. Before I explain what that means in more detail, let me show you an example (cards.tcl in this chapter’s code directory):

# Return a random integer between 0 and the number of elements in
# the list specified by cardList
proc Random {list} {
    set index [expr {int(1 + rand() * ([llength $list]) - 1)}]    return $index
}

set values [list Ace King Queen Jack 10 9 8 7 6 5 4 3 2]
set suits [list Clubs Diamonds Hearts Spades]
lappend cards $values $suits

# "Deal" a draw poker hand
for {set i 1} {$i <= 5} {incr i} {
    puts "Card $i: [lindex $cards 0 [Random $values]] of
        [lindex $cards 1 [Random $suits]]"
}

When you execute this script, the output will be a (reasonably) random hand of draw poker:

$ ./cards.tcl
Card 1: Queen of Diamonds
Card 2: 3 of Diamonds
Card 3: 9 of Spades
Card 4: 10 of Hearts
Card 5: 5 of Diamonds

Yes, this poker hand is a loser. To understand how multi-valued index arguments work, consider the first lindex command in the example, [lindex $cards 0 [Random $values]]. Next, break it down into its components. $cards is the list variable from which I want to extract a value. $cards is a two-element list; the first element is the sublist $values ({Ace King Queen Jack 10 9 8 7 6 5 4 3 2}), and the second element is the sublist, $suits ({Clubs Diamonds Hearts Spades}).

The index values are 0 and the return value of the procedure, [Random $values]. As you can see from the comment above its definition, my Random procedure returns a random integer between 0 and the number of elements in the list passed to it. I only specified two indices, which means that the first index, 0, selects the sublist from which the second index, the return value of [Random $values], selects the desired element.

Given that the sublist of $cards at index 0 is {Ace King Queen Jack 10 9 8 7 6 5 4 3 2}, if [Random $values] returns 2, then the entire expression evaluates to King. To express it another way, the command, [lindex $cards 0 [Random $values]] is equivalent to and a more concise way of writing the following commands:

set sublist [lindex $cards 0]
set index [Random $values]
set suit [lindex $sublist $index]

I readily concede that specifying multi-valued indices to lindex is confusing. Using multivalued indices is most appropriate when you are working with nested lists, that is, lists containing elements that are themselves lists. If you are more comfortable using multiple commands to achieve the same result, do so. As you grow more comfortable with Tcl, it will become more natural to use multi-valued indices. More importantly, you might encounter such expressions in other people’s Tcl code, so you’ll need to be able to parse and understand such code, even if you don’t like it or use it yourself.

Happily, the command to return multiple consecutive elements from a list, lrange, is much less subtle than lindex. By way of reminder, lrange’s syntax is:

lrange listVar start end

start and end indicate the first and last indices, respectively, of the values which lrange should return. lrange returns a new list that consists of the elements between start and end, inclusive. Consider the following short script (lrange.tcl in this chapter’s code directory):

set values [list Ace King Queen Jack 10 9 8 7 6 5 4 3 2]
set suits [list Clubs Diamonds Hearts Spades]

puts [lrange $values 5 8]
puts [lrange [list $values $suits] 1 2]

The output should resemble the following:

$ ./lrange.tcl
9 8 7 6
{Clubs Diamonds Hearts Spades}

The first lrange command returns the elements between indices 5 and 8 (remember, list indices are zero-based). The second lrange returns the elements between indices 1 and 2 of the two-element list created by the embedded list command. However, because there is no index 2 (that is, no third element), lrange treats that value as if it were end, which refers to the last element in the list. Similarly, if the starting index is less than zero, lrange will treat it as if it were 0 and return the first element in the list.

Modifying Lists

Your options for modifying lists include lappend, which you’ve already seen, linsert, lset, and lreplace. linsert inserts one or more new elements into a list, lset sets (changes) the value of one or more specific list elements, and lreplace replaces list elements with new elements.

Inserting New Elements

To insert one or more elements into a list at a specific location, use the linsert command. Its syntax is:

linsert listVar index item ?item ...?

This command inserts each new element (denoted by item) into the list specified by listVar immediately before the index specified by index. listVar itself is not modified. Rather, linsert returns a new list with the inserted values. If index is less than zero, the new elements will be inserted at the beginning of the list (at index 0); if greater than the number of elements in the list, the new elements will be appended to the end of the list. You can specify the end of the list using the special index value end.

The following example demonstrates linsert usage (see linsert.tcl in this chapter’s code directory):

set oldList [list 1 2 3 4 5]
set newList [linsert $oldList 0 0]
set newerList [linsert $newList 2 2.5]
set newestList [linsert $newerList [expr [llength $newerList] + 1] 6]

puts "oldList   : $oldList"
puts "newList   : $newList"
puts "newerList : $newerList"
puts "newestList: $newestList"

The second linsert command inserts the element 0 at the front of the list. The third linsert command inserts the element 2.5 in the middle of the list (before index 2). The final linsert command adds the element 6 to the end of the list. In this command, I deliberately set the insertion index to a value greater than the length of the list ([expr [llength $newerList] + 5] evaluates to 10) to show how linsert treats an index value greater than the length of the list.

If you execute this script, the output should match the following:

$ ./linsert.tcl
oldList   : 1 2 3 4 5
newList   : 0 1 2 3 4 5
newerList : 0 1 2.5 2 3 4 5
newestList: 0 1 2.5 2 3 4 5 6

Notice that the source lists, the lists passed to linsert as arguments, are not modified.

Replacing Elements

To replace one or more list elements with new ones, use the lreplace command. Its syntax is:

lreplace listVar start end ?item ...?

lreplace returns a new list created by replacing the element or elements between index values start and end in listVar with the elements specified by item. Omitting item has the effect of deleting the corresponding element from the list. If you specify fewer items than there are indices between start and end, the excess elements in listVar will be deleted from the returned list. Similarly, specifying more items than there are indices results in inserting the extra elements following the last replaced item, effectively expanding the list at that point. As with other list commands, if start is less than zero, it will be treated like zero. If end is less than start, all of the specified elements will be inserted at the beginning of the list without replacing existing list elements. Finally, if the list specified by listVar is empty, all of the specified items will be appended to the list. Despite all of the niggling evaluation rules, lreplace behaves the way you would expect it to. The following trivial example illustrates simple lreplace usage (see lreplace.tcl in this chapter’s code directory).

set oldList [list 1 2 3 4 5]

set newList [lreplace $oldList 0 end one two three four five six]

puts "Original list: $oldList (length: [llength $oldList])"
puts "Replaced list: $newList (length: [llength $newList]);"

Searching and Sorting Lists

Once you’ve created, appended, inserted, retrieved, or replaced list elements, chances are pretty darn good that you’ll want to be able to search and sort list elements. The commands for doing so are, not surprisingly, lsearch and lsort. I’ll discuss lsearch first and then proceed to lsort.

Searching 101

lsearch’s general syntax is:

lsearch ?option ...? listVar pattern

lsearch searches each element of listVar for a match with the specified pattern. It returns the index of the first matching element by default or -1 if there are no matches. The option arguments control how the match is performed and the nature of lsearch’s return value. The only default option is -glob, which forces a glob-style match using the same rules as the string match command (described in Chapter 4). Table 5.1 lists the permissible options.

Table 5.1. lsearch Options

Option

Description

-all

Returns all matching indices; cumulative with -inline.

-ascii

Used with -exact or -sorted, compares list elements as Unicode strings.

-decreasing

Used with -sorted, indicates that the search list is sorted in decreasing order.

-dictionary

Used with -exact or -sorted, compares list elements using dictionary-style matches.

-exact

Forces the match to contain exactly the same string as pattern.

-glob

Forces a glob-style match following the same rules as string match.

-increasing

Used with -sorted, indicates that the search list is sorted in increasing order.

-inline

Returns all matching values; cumulative with -all.

-integer

Used with -exact or -sorted, compares list elements as integers.

-not

Negates the match, returning the index of the first non-matching element.

-real

Used with -exact or -sorted, compares list elements as real numbers (that is, as floating point values).

-regexp.

Treats pattern as a Tcl regular expression when evaluating matches.

-sorted

Indicates that the search list is sorted; cannot be used with -glob or -regexp.

-start index

Starts the search at the list index specified by index.

Don’t let the number of options intimidate you. Most of them have to be used in combination with -sorted to even apply. You can perform three types of searches:

  • String-style glob searches (-glob, the default)

  • Regular expression searches (-regexp)

  • Exact match searches (-exact)

By default, lsearch returns the index of the first match. If you want the index values of all matches, specify -all and lsearch will return a list of matches. If you want the elements themselves rather than their indices, specify -inline. Again, unless you also specify -all, -inline will return only the first match. Finally, to start the search at a specific index, rather than at the beginning of the list, specify -start index, where index is the index value at which to commence the search.

The balance of the options assume sorted search input, specified with -sort. Why does this matter? As an optimization, if lsearch knows the input is sorted, it can use a search algorithm best suited to the input rather than a general purpose, one-size-fits-all search algorithm. The most interesting option is -not, which inverts the sense of the search and returns the first non-matching index or value (or all of them if you also specify -all).

Tip: Sort Before Searching

Tip: Sort Before Searching

If you need to search a long list, make sure it is sorted first. lsearch does not sort unsorted lists, so you can improve search speed by sorting the list (using lsort) before passing it to lsearch.

I could spend an entire chapter on lsearch alone and not even talk about the -regexp search option. I encourage you to experiment with lsearch and its options to get a better sense of what it can do. To help you out in that respect, programs you encounter later in this book will use a number of lsearch’s options, so you will definitely get plenty of lsearch goodness. In the meantime, the following script illustrates basic lsearch usage:

# Create a list of cards
set cards [list "Queen of Hearts" "3 of Clubs" "9 of Spades" 
    "Ace of Hearts" "5 of Diamonds"]

# Ask user for what to search
puts -nonewline "Card for which to search (such as King, Spade, or 9): "
flush stdout
gets stdin card

# Loop until user inputs a matching card
while {[set index [lsearch $cards *[string totitle $card]*]] < 0} {
    puts "No such card. Please try again."
    puts -nonewline "Card for which to search (such as King, Spade, or 9): "
    flush stdout    gets stdin card
}

# Show the match
puts "Matched the [lindex $cards $index]"

This script creates a list of five cards, asks the user to enter some text that describes a card for which to search, and then searches the list for that card. The prompt-search routine repeats until lsearch finds a match. Upon finding a match, the while loop terminates and the script displays the matching card. Here’s how the output might look (see lsearch.tcl in this chapter’s code directory):

$ ./lsearch.tcl
Card for which to search (such as King, Spade, or 9): 4
No such card. Please try again.
Card for which to search (such as King, Spade, or 9): King
No such card. Please try again.
Card for which to search (such as King, Spade, or 9): queen
Matched the Queen of Hearts

I used the string totitle command to uppercase the first letter of the user’s input. The search pattern, rather, the search glob, is stored in the $card variable. To allow matches to occur in the middle of an element, I added * to the beginning and end of the search pattern so the glob would match the pattern, regardless of where in the element the pattern appears. Finally, notice the loop termination condition. lsearch returns -1 if it doesn’t find a match, so the test condition simply checks for a return value that is less than zero. Obviously, you could check for -1, too.

Tip: On Naming Variables

Tip: On Naming Variables

In lsearch.tcl, I used the plural noun $cards to refer to the list of cards as a whole and the singular noun $card to denote a specific card. This is a convention, or idiom, I use frequently. Most, if not all of my variable names are nouns because, based on my own experience, the “things” that variables represent are usually nouns. When I need to refer to collections (you know, collections like lists) of related values or items, I use the plural form of the noun, such as cards, books, dice, games, and so on. When I need to refer to a particular member or instance of that collection, I use the singular form of the noun (card, book, die, game). The reason I do this is that I can determine at a glance what a variable is and how I’m using it.

Sorting

You probably noticed that a number of lsearch’s options involved searching a sorted list. Searching for something, in code and in real life, is much more efficient if the material you’re searching is already sorted. To accomplish this in Tcl, use the lsort command. Its general syntax is:

lsort ?options? listVar

lsort sorts listVar and returns a new sorted list. options control how the sort is performed. Not surprisingly, lsort and lsearch share a number of options. Table 5.2 lists lsort’s options, with bold items representing the defaults when no options are specified.

Table 5.2. lsort Options

Option

Description

-ascii

Performs the sort using Unicode strings.

-dictionary

Performs the sort using dictionary-style sorting.

-integer

Performs the sort by comparing the elements as integers.

-real

Performs the sort by comparing the elements as floating-point numbers.

-command command

Performs the sort using command(a Tcl command) to compare elements.

-increasing

Sorts the elements in increasing or ascending order.

-decreasing

Sorts the elements in decreasing or descending order.

-index index

Sorts sublists on the specified index rather than sorting the entire list.

-unique

Eliminates all but the last set of duplicate elements in the source list.

A dictionary sort is handy when the list elements contain mixed alphanumeric values, such as a10 and a2. In the standard (-ascii) sort, a10 would sort before a2 because the first two characters of a10, a1, are “less than” a2. With a -dictionary sort, the numbers are treated as integers, so a2 would sort before a10 because 2 is less than 10.

The -index option needs elaboration. It exists to handle properly sorting lists that consist of sublists. When specified, -index index performs the sort by sorting the list as a whole on the indexth element of each sublist. For example, given the following list of cards, the default sort is by the card value, that is, by index 0:

% lsort {{King of Diamonds} {Ace of Hearts} {10 of Clubs}}
{10 of Clubs} {Ace of Hearts} {King of Diamonds}

If you want to sort by suit, use the option -index 2 to sort by the third element of each sublist:

% lsort -index 2 {{King of Diamonds} {Ace of Hearts} {10 of Clubs}}
{10 of Clubs} {King of Diamonds} {Ace of Hearts}

The -unique option is a great way to eliminate duplicates from a list. However, it has a subtlety that might bite you if you also specify -index. If you are sorting a list of sublists, the default sort is to sort by each element of each sublist. If you specify -index and - unique and your list contains two sublists with the same element at the same index, only the last one will appear in the sorted list, regardless of the values of the rest of the elements. To illustrate, compare the results of the following three commands:

% lsort -unique {{King of Hearts} {King of Diamonds} {2 of Clubs}
{2 of Clubs} {King of Diamonds} {King of Hearts}
% lsort -unique -index 0 {{King of Hearts} {King of Diamonds} {2 of Clubs}}
{2 of Clubs} {King of Diamonds}
% lsort -unique -index 1 {{King of Hearts} {King of Diamonds} {2 of Clubs}}
{2 of Clubs}

The first lsort command shows the default behavior. The second one shows the result if you sort the list on index 0 alone: the resulting list drops the second King (the King of Hearts). As a pathological example, the third lsort sorts on index 1 (the word of in each sublist), which results in dropping the Kings from the sorted list.

The following script, lsort.tcl in this chapter’s code directory, shows a simple example of lsort’s usage. It uses the rand() function to generate a list of five random numbers and then lsort to sort them, displaying both the unsorted and sorted lists.

# Generate a list of 5 floating point numbers
for {set i 0} {$i < 5} {incr i} {
    lappend floats [expr rand()]
}

puts "Unsorted list:"
foreach float $floats {
    puts "	$float"
}

set s_floats [lsort -real $floats]

puts "
Sorted list:"
foreach s_float $s_floats {
    puts "	$s_float"
}

The only remarkable feature here is that I use the -real option to force the sort to be performed using floating-point comparisons rather than the default Unicode comparisons. Well, I also used the foreach looping command about which you’ll read shortly. When you execute this script, you should see output resembling the following (of course, the list elements will differ):

$ ./lsort.tcl
Unsorted list:
    0.95407820258
    0.192350764383
    0.839296981617
    0.0643700450027
    0.867346360752
Sorted list:
    0.0643700450027
    0.192350764383
    0.839296981617
    0.867346360752
    0.95407820258

Additional List Operations

The example scripts in this chapter are contrived in that they create lists specifically for the purpose of demonstrating this or that feature of list manipulation. The code you write will sometimes permit you to do the same. However, in many real-world programs, including games, you won’t have the luxury of crafting your data when writing a script. Rather, you have to take the data you’re handed and coerce it into the proper format. The last two list operations you need to know are how to convert a plain vanilla string using the split command and how to turn a list into a plain vanilla string using the join command.

Strings to Lists

Given a string that you want to convert to a Tcl list, the split command makes short work of the task. Its syntax is:

split $string ?chars?

split returns a list created by breaking string into elements at each occurrence of the split characters specified in chars. If you omit chars, the default split character is whitespace. Yes, you can specify multiple split characters in chars. Each occurrence of adjacent or consecutive char results in an empty list element. split also generates an empty list element if the first or last character of string matches a split character.

The following command breaks the sentence “A straight flush beats a full house.” into a list consisting of each word in the sentence:

set words [split $sentence]

If, perversely, you want to break the sentence at, say, the letters a and s, the following command would do:

set words [split $sentence {as}]

Here is the output of both commands. (See split.tcl in this chapter’s code directory.) Notice that the result of the second split command is grouped with braces. This occurs because the resulting list elements contain embedded whitespace, the default list separator. Tcl does this to protect the spaces and maintain the integrity of the resulting list.

$ ./split.tcl
A straight flush beats a full house.
{A } tr {ight flu} {h be} t { } { full hou} e.

Be careful when using split on arbitrary input because stray double quotes or braces will cause an error (see bad_split.tcl in this chapter’s code directory):

% puts [split "A straight {flush "beats a full house."]
extra characters after close-quote

Tip: Breaking Strings into One-Character Lists

Tip: Breaking Strings into One-Character Lists

If you want to convert a string into a list of single-character elements, specify the empty list, {}, as the split character:


% split {Lots of characters} {}
L o t s { } o f { } c h a r a c t e r s

As you can see, split is a command that is easy to understand and use, as well as being capable and powerful.

Note: You Don’t Always Need split

Note: You Don’t Always Need split

In many situations, you don’t need to use split to make a list. A string is a list and a list is a string. For example, you can use the list operation lindex on the string “A straight flush beats a full house.” without first using split. In fact, history buffs might appreciate the fact that the string length command was added to eliminate the hack of using llength on a string to test whether or not the string was empty.

Lists to Strings

join performs the inverse operation of split, converting a list into a string and return the new string. join’s syntax is:

join listVar ?chars?

join works by converting each element of listVar in a string, with each element separated by the character or characters specified by chars. As with the split command, chars can be a list of multiple characters. For example, in the following example (join.tcl in this chapter’s code directory), the first join command splits the list into a single string whose words are separated by newlines, and the second one creates a string whose values are separated by a comma and a space:

% join {Ace King Queen Jack 10} "
"
Ace
King
Queen
Jack
10
% join {{A a} {B b} {C c} d E} ", "
A a, B b, C c, d, E

In the second command, the embedded sublists, such as {A a}, were not joined into strings; join only strips off a single level of list structure.

Looping with the foreach Command

Earlier in the chapter, I sneaked the foreach looping command past you. As promised, I’m following up with a proper explanation. foreach is a specialized form of a loop specially designed for iterating over items in a list. The syntax for the form that you will use most often is:

foreach varName listVar {body}

foreach iterates over each element in varList by assigning an element to varName and then executing body. The loop terminates after executing body for the last element of varList. The assumption is that body does something with the list elements, but it doesn’t have to. The following example shows how you might use foreach (see foreach.tcl in this chapter’s code directory):

set cards [list Ace King Queen Jack 10]
foreach card $cards {
    lappend newCards [string toupper $card]

}
puts $newCards

If you rewrote the foreach loop to use a standard for command, it would be more verbose:

for {set i 0} {$i <= [llength cards]} {incr i} {
    lappend newCards [string toupper $card]
}

I think you’ll find that foreach is more compact to write and much more expressive than the equivalent for loop.

foreach’s general syntax is slightly more complex because you can iterate over multiple lists:

foreach varName listVar ?varNameN listVarN ...? {body}

This is an advanced usage that I won’t use in this book, but the basic idea is that on each iteration of the loop, one element from each list is assigned to its corresponding loop variable and then the loop body is executed, presumably using or modifying the loop variables. The potential gotcha here is that unless you code defensively, you’ll wind up with unexpected results or outright errors. Why? If one of the lists has more elements than the other, the loop variable for the shorter list will be assigned an empty value for each missing element. Coding defensively in this case means the loop body must at least accommodate empty values appropriately.

Another commonly used variation of foreach loops is to pull multiple elements off a list in a single iteration. This technique is often used to convert lists to arrays and arrays to lists. The following script, pairs.tcl in this chapter’s code directory, illustrates a similar use:

set cards [list Ace Clubs King Hearts Queen Spades Jack Diamonds 10 Clubs]

foreach {card suit} $cards {
    puts "$card of $suit"
}

This script pulls two values off the list variable $cards on each iteration, storing the fetched values in the $card and $suit variables, making it trivial to display nicely formatted card names:

$ ./pairs.tcl
Ace of Clubs
King of Hearts
Queen of Spades
Jack of Diamonds
10 of Clubs

Conditional Execution: The switch Command

Back in Chapter 3, you learned how to use the if command to execute a given block of code conditionally. You also learned that you could use as many elseif clauses as necessary to handle multiple conditions. At the time, I wrote “If you need very many [elseif clauses], you’ll probably want to use the switch command. More than four or five elseif clauses looks messy and can be difficult to maintain.” So, about the switch command...

switch, like if, branches the flow of control in a program to one of many blocks of code based on the value of an expression. One of the most common situations in which you’ll use it is to execute a given code block in response to user input, such as you might get from a menu. Another frequent use of switch is in event-driven programs (such as those you create with Tk). In event-driven programs, the main program runs in a loop and waits for an event to occur, such as a mouse click, a keypress, or the completion of a long-running process. When an event occurs, the main program executes code to handle or respond to that event.

switch’s general syntax is one of the following (I’ll explain why there are two possibilities in a moment):

switch ?option ...? value pattern body ?pattern body? ?...?
switch ?option ...? value {pattern body} ?{pattern body}? ?...?

switch compares value to each pattern sequentially and, when it finds a match, executes the corresponding body. Upon completion of the associated body, switch returns the result of that body. If no matching pattern is found and the last pattern is not the special pattern default, switch returns the empty string. If the last pattern is default, it matches any pattern. option, of which there can be multiple, modifies switch’s behavior. It can be one of the following:

  • -exact—. Uses exact matching when comparing value to pattern (this is the default).

  • -glob—. Uses glob-style matching when comparing value to pattern; this is the same glob-bing as the string match command supports.

  • -regexp—. Uses regular expression matching when comparing value to pattern.

  • --—. Signals the end of options; this is necessary so that the value argument can begin with a single hyphen and not be interpreted as an (invalid) option.

Why are there two syntax possibilities, one without braces and one with? The first form, the one without braces, allows substitution to occur in the patterns, which is good, but requires backslashes if you want the switch command to span multiple lines, which is a pain. I usually want the switch command to span multiple lines to improve readability. The second form, the one with braces, prevents substitutions from occurring in the patterns, which is potentially bad, but eliminates the need for escaping the newlines.

It’s an unfortunate dilemma: Do you want your code to be readable or do you want substitutions in your pattern arguments? With careful coding, you might be able to arrange for the patterns to be substituted before you enter a switch block. However, it might be easier to opt for the brace-infested form and put up with unsightly code. As you gain experience reading and writing Tcl code, backslash-escaped newlines will become more familiar and won’t be so visually jarring, In addition, my opinion is that the power and expressiveness that command and variable substitution imparts to Tcl is worth the inconvenience of having to escape newlines. Finally, using both syntaxes as the situation requires is preferable to and more practical than being doctrinaire and always using one or the other.

Tip: Always Group Command Bodies in switch Blocks

Tip: Always Group Command Bodies in switch Blocks

Regardless of which of the two switch syntaxes you use, you should always group the command bodies. This is a matter of efficiency. If all the command bodies are grouped (using braces), no substitutions occur until control enters the body that corresponds to the matching pattern, and substitution will only occur in that body.

Examples? nobrace.tcl in this chapter’s code directory shows you how to use the no-brace-ugly-backslashes syntax:

# Create menu
set menu [list {S: Save game} {Q: Save game and exit}
{X: Exit without saving} {N: Start new game} {C: Return to current game}]

# Show the menu
foreach option $menu {
    puts $option
}

# Get user's input
puts -nonewline "Choice [SQXNC]: "
flush stdout
gets stdin choice

# Process the input
switch -exact -- [string toupper $choice] 
    S {puts "Game saved"}
    Q {puts "Game saved. Exiting"}
    X {puts "Exiting immediately"}
    N {puts "Starting new game"}
    C {puts "Returning to current game"}
    default {puts "Invalid option: $choice"};

This script mimics a game menu from which players can save a game, exit the game, and so on. Players simply press a letter corresponding to the option they want:

$ ./nobrace.tcl
S: Save game
Q: Save game and exit
X: Exit without saving
N: Start new game
C: Return to current game
Choice [SQXNC]: s
Game saved
$ ./nobrace.tcl
S: Save game
Q: Save game and exit
X: Exit without saving
N: Start new game
C: Return to current game
Choice [SQXNC]: p
Invalid option: p

Strictly speaking, this example could have been written to use braces around the pattern body pairs because the patterns do not use variables.

Interrupting Loop Execution

When executing a loop, it is sometimes necessary to interrupt execution before the next iteration of the loop. In some cases, you want to exit the loop without executing any more of the loop code. In other situations, you want to start the next iteration immediately. To terminate a loop prematurely, use the break command; to start the next iteration of a loop, use the continue command.

Suppose that you are writing a program to play Blackjack (a/k/a 21) and are using a while loop to handle dealing the cards. If a player draws a Jack and an Ace, that player’s hand is an immediate winner, and there is no need to deal more cards. In this case, you would use the break command to terminate the loop because you no longer need to give the player any more cards. The syntax of the break command is just that, a bare break command:

break

Here’s a short script that illustrates the break command:

for {set i 1} {$i <= 10} {incr i} {
    # Generate a random number between 1 and 21 inclusive    set num [expr int(1 +
(rand() * 21))]
    if {$num == 21} {
        break }
}

if {$num == 21} {
    puts "Got 21 on iteration #$i"
} else {
    puts "Didn't get 21"
}

This script generates a random number between 1 and 21 in a for loop that iterates a maximum of 10 times. If the generated number is 21, the break command terminates the loop; otherwise, it generates another random number and tries again. If you execute this script (break.tcl in this chapter’s code directory), the output should resemble the following. You might have to run it a couple of times before you hit 21:

$ ./break.tcl
Didn't get 21
$ ./break.tcl
Got 21 on iteration #7

Suppose, on the other hand, you are using a foreach loop to iterate over a list of inventory items and are looking for a specific item, say, a knife, and the logic in the loop deals contains code describing what to do with that knife. For each item that isn’t a knife, you can use the continue command to start the next iteration of the loop because there is no need to execute the rest of the code. Like break, continue’s syntax is just the command continue:

continue

The script continue.tcl in this chapter’s code directory shows continue in action:

set inventory [list club sword crossbow arrows knife dagger bow]

foreach item $inventory {
    if {$item ne "knife"} {
        continue }
    puts "Using knife to pry open door to safe"
}

Even though the puts statement is not protected by an else clause, it will only execute when the inventory item is a knife. Each item that isn’t a knife causes the continue command to execute, which starts the next iteration of the foreach loop and skips the puts command.

Analyzing Playing Blackjack

blackjack.tcl is a good starting point for full-featured Blackjack games. It also demonstrates “real world” usage of the major features of lists and the control structures I introduced in this chapter. It is somewhat long, but that’s mostly because there is a lot of repetitive code. Once you learn more about procedures (Chapter 7, “Writing Tcl Procedures”), you’ll be able to replace repetitive code blocks with procedures. In the meantime, don’t let the length intimidate you.

Looking at the Code

#!/usr/bin/tclsh
# blackjack.tcl
# Play a hand of Blackjack

# Return a random integer between 0 and the number of elements in
# the list specified by list
proc Random {list} {
    set index [expr {int(1 + rand() * ([llength $list]) - 1)}]    return $index
}

# Block 1
# Create a deck of cards
set values [list Ace King Queen Jack 10 9 8 7 6 5 4 3 2]
set suits [list Clubs Diamonds Hearts Spades]
lappend deck $values $suits

# Block 2
# Deal 2 cards
for {set i 1} {$i <= 2} {incr i} {
    lappend cards "[lindex $deck 0 [Random $values]] of
        [lindex $deck 1 [Random $suits]]"
}

# Display the initial hand
foreach card $cards {
    puts $card
}

# Block 3
# Deal another?
puts -nonewline "Deal another card [yn]? "
flush stdout
gets stdin answer
while {[string tolower $answer] eq "y"} {
    lappend cards "[lindex $deck 0 [Random $values]] of
        [lindex $deck 1 [Random $suits]]"    foreach card $cards {
        puts $card    }
    puts -nonewline "Deal another card [yn]? "    flush stdout    gets stdin
    answer
}

# Block 4
# Score the hand
set score 0
foreach card $cards {
    switch -glob -- $card 
        Ace* {set value 11}
        King* -
        Queen* -
        Jack* -
        10* {set value 10}
        default {set value [lindex $card 0]}    set score [expr $score + $value]
}

# Block 5
# Display the score
puts -nonewline "$score: "
if {$score > 21} {
    puts "Bust!"
} elseif {$score == 21} {
    puts "Perfect!"
} elseif {$score >= 16 && $score <= 20} {
    puts "Tough hand to beat!"
} else {
    puts "Better luck next time!"
}

Understanding the Code

You’ve already seen the procedure, Random, at the top of the script (refer to the cards.tcl script in the section titled “Accessing Specific List Elements”). It generates a random number between 0 and the number of elements in a list. I use this procedure in Blocks 2 and 3 to deal a reasonably random card. Block 1 just creates a “deck” of cards out of which the script will deal cards—you saw this card in cards.tcl, too. So far, nothing new.

In Block 2, I deal the first two cards, storing them in the list variable named cards. The foreach loop sets the stage for the rest of the game by displaying the player’s initial hand of cards.

The game gets going in Block 3. Now that the user has had a chance to examine his cards, I use the standard input sequence I’ve used throughout the first few chapters of this book to ask the user if he would like another card. If the user types “n,” control moves to the code in Block 4. Otherwise, control enters the while loop, which:

  1. Deals another card.

  2. Redisplays the hand with the new card.

  3. Repeats the prompt to deal another card.

The while loop continues until the user types “n.” Notice the use of string tolower to make sure that the user can type “y” or “Y” for yes. Another feature of the while loop is the prompt itself. Because I need literal [ and ] characters, I have to escape them using backslashes. Without the escapes, the interpreter would try to execute a command or procedure named yn and would generate an error that aborts the script.

After all the cards have been dealt in Block 3, Block 4 scores the hand. I use a foreach loop to iterate through the cards list and assign a score to each card using a switch command. The -glob option was necessary because each card has both a denomination (such as Ace or 8) and a suit. For example, the glob pattern Ace* matches “Ace of Spades,” “Ace of Clubs,” and so forth. The score for each card is stored in the variable value, which is added to the total score, stored in the variable score, at the bottom of the loop.

The scores for Ace and 10 are obvious, but the handling of the other face cards and of the other numeric cards is a little trickier. The command body for a King, Queen, or Jack is -. This means that the body for the following pattern should be used for this pattern. The idea is to share command bodies among several patterns. In this case, the command body for a pattern of 10 will be used for patterns that match King*, Queen*, Jack*, and, of course, 10*. In effect, a body of allows execution to fall through to a common body.

For the numeric cards, I score them by setting value to the numeric component of the list element (the numeric cards look like “3 of Clubs” or “8 of Spades”). I extract the numeric component using the command lindex $card 0. Once again, you see how nested commands and command substitution make it possible, even trivial, to build powerful compound commands with a minimum of code.

After adding the score of the current card to the total score, the scoring process continues with the next element of the list/card in the user’s hand. When all cards have been scored, Block 5 displays the score along with a short message and the script exits.

Modifying the Code

Here are some exercises you can try to practice what you learned in this chapter.

  • 5.1 Add code to prevent dealing the same card twice.

  • 5.2 Add code to give the user the choice of scoring Aces as 1 or 11.

  • 5.3 Add code to handle input that isn’t y or n.

  • 5.4 Add code to test for a blackjack and exit the foreach loop if it is.

Lists are one of Tcl’s two native data structures. The Blackjack game illustrates some of what Tcl’s list-related functionality makes possible. Given data that lends itself to arrangement in a list, Tcl has commands for creating the list, accessing particular list elements, sorting the entire list or only part of it, searching a list for elements that match certain criteria, and adding, deleting, or modifying list elements. In addition, you can convert between lists and strings with the split and join commands. The foreach loop control structure is specifically designed to iterate over lists, and you can even control loop execution by using the break and continue commands. In the next chapter, you’ll learn how to use Tcl’s other native data structure, the array.

 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.3.154