switch

Difference between version 107 and 108 - Previous - Next
'''[http://www.tcl.tk/man/tcl/TclCmd/switch.htm%|%switch]''', a [Tcl Commands%|%built-in] [Tcl] [command], uses patterns to select a script, and then [eval%|%evaluates] that script.



** Synopsis **

    :   '''switch''' ?''options''? ''string pattern body'' ?''pattern body ...''? 

    :   '''switch''' ?''options''? ''string'' {''pattern body'' ?''pattern body ...?}



** Documentation  **

   [http://www.tcl.tk/man/tcl/TclCmd/switch.htm%|%official reference]:   



** See Also **

   [ranged switch]:   A range-aware lookalike to `switch`.

   [switch-regexp]:   `switch` with `[regexp]` variable capturing.



** Description **

`switch` supersedes `[case]`.

When the ''pattern body'' arguments are presented as separate arguments rather
than as one single argument, and ''string'' starts with or could possibly
start with `-`, the `--` option should be used to ensure that ''string''
is not incorrectly treated as an option. 

======
switch -- $foo {...}  ;# robust for all values of foo
======

In Tcl versions prior to [Changes in Tcl/Tk 8.5%|%8.5],  `--` should be used
in any case.

With exactly one argument, that argument is a [dict%|%dictionary] of patterns
and their corresponding bodies.

`switch` only performs string comparison, so it isn't ideal for numeric
comparison.  That said, `switch` almost always "just works" for the numeric
cases because string comparison is a natural surrogate for equality testing of
integers, and the cost of creating a string representation of an integer is
usually nominal in the grand scheme of a Tcl script.  `[if]` is always
available for numeric or other more complex tests.



** Examples **

   [cmdSplit%|%wordparts]:   iIllustrates both `-regexp` and `-matchvar` options.



** Attempts to get Substitution in the Single-Argument Syntax **

Coding style for switch is tricky.  Recently on [comp.lang.tcl] there was
an exchange about getting `switch` to work right.  The user had originally
typed:

======
switch $::errorCode {
    $NOTFOUND {
        puts {page not found}
    }
    default {
        puts "Program encountered code $::errorCode"
        break
    }
}
======

and was frustrated because the application kept taking the default branch.

[PYK] 2014-05-29: [scripted list] can make this syntax work, and even allows comments:

======
switch $::errorCode [sl {
    #todo: offer a list of similar pages
    $NOTFOUND {
        puts {page not found}
    }
    default {
        puts "Program encountered code $::errorCode"
        break
    }
}]
======

A well-known Tcl pundit suggested:

======
switch $::errorCode "
    $NOTFOUND {
        puts "page not found "
    }
    default {
        puts "Program encountered code $::errorCode"
    }
"
======
so that Tcl had a chance to substitue `$NOTFOUND`.

''[AMG]: The quotes don't match up the way you expect.  This is because braces inside "quoted" words don't have any special power.  the second argument to switch starts with a line break, ends with the space following puts, and has the word "page" illegally appended to it, resulting in an "extra characters after close-quote" error.''

However, the follow up, by [Ken Jones] said it best:

you don't want that (unless you're doing some very, very, very
tricky stuff). Quoting switch's pattern/action block with `""` allows
substitutions within the entire argument. So, yes you get `$URL_NOT_FOUND`
substituted. But you also get the contents of the actions
substituted before switch even gets called.

[lexfiend]: I think the following example illustrates Ken's point better:

======
# GOTCHA - You're dead no matter what you do
set TriggerBomb 0
set DefuseBomb 1
set WaitAMinuteWhileIThinkThisThrough 2
set Action $WaitAMinuteWhileIThinkThisThrough
interp alias {} SendToBomb {} puts
switch $Action "
    $TriggerBomb {
        set result [SendToBomb boom]
    }
    $DefuseBomb {
        set result [SendToBomb shutdown]
    }
"
======

Instead, we want the syntax of `switch` where the 
patterns corresponding bodies are all separate arguments,
rather than a single list of alternating pattern/body items. Here's a
correctly working example of the above code:

======
set NOTFOUND value
set errorCode nonsense

switch -- $::errorCode \
    $NOTFOUND {
        puts {page not found}
    } \
    default {
        puts "Program encountered code $::errorCode"
    }
======

[KBK]: I dislike both of these.  I tend to think of a switch as a
selection among fixed alternatives, and substitution in the cases
confuses me.  I'd have just written:

======
if {$::errorcode eq $NOTFOUND} {
    puts {page not found}
} else {
    puts "Program encountered code $::errorCode"
}
======

[RHS]: I find that when I need substitution for switch values, formatting it like the following looks cleaner:

======
switch -- $::errorCode $NOTFOUND {
    puts {page not found}
} $value1 {
   puts blah
} default {
   puts "Program encountered code $::errorCode"
}
======

[DKF]: With 8.6, I'd be using `[try]` for this particular problem instead of using `switch` to pick through [errorCode%|%$::errorCode] after `[catch]` returned, but YMMV.

----

[LV]: The above sample was, of course, intended to be brief, and didn't show the other several dozen switch cases.  For a single comparison, I agree with [KBK].  But if I have more than one or two of these types of comparisons, then I know ''I'' prefer a switch.

----

How's this for a silly compromise.
Nicely aligns the dispatch conditions and removes the confusing barrage of backslashes.

======
if 0 {
} elseif {$state eq $INIT_STATE} {
   puts init
} elseif {$state eq $CLEANING_STATE} {
   puts cleaning
} elseif {$state eq $DONE_STATE} {
   puts done
}
======

----

The silliness continues... this actually works though I'm not sure of the side effects.

======
proc dispatch {thevar args} {
    set pass1 [uplevel subst $args]
    set pass2 "switch \$$thevar \{ $pass1 \}"
    uplevel $pass2
}

set INIT_STATE init
set CLEANING_STATE cleaning
set DONE_STATE done

set state $INIT_STATE

dispatch state {
    $INIT_STATE {
        puts init
        set nextstate $DONE_STATE
    }
    $CLEANING_STATE {
        puts cleaning
    }
    $DONE_STATE {
        puts done
    }
    default {
    }
}
puts "next state $nextstate"
======

[PYK] 2014-05-30: The code above is not robust due to the use of `[subst]`
.


======
set INIT_STATE init
set CLEANING_STATE cleaning
set DONE_STATE done

dict set body [list $INIT_STATE] {
    puts init
    set nextstate $DONE_STATE
}
dict set body [list $CLEANING_STATE] {
    puts cleaning
}
dict set body [list $DONE_STATE] {
    puts done
}
dict set body default {
}

set state $INIT_STATE

switch $state $body

puts "next state $nextstate"
======


** `-regexp` and `default` **

[PZ]: Using `default` alternative with `-regexp`:

======
set a x
switch -regexp -- $a {
    y {
        puts y
    }
    {} {
        puts z
    }
    
    default {
        puts $a
    }
}
======

returns:

======none
z
======

This is because `{}` matches the [empty string] that implicitly exists at both
ends of a string and between every character in a string.



** Match Abbreviations **

With `switch -regexp`, you can specify abbreviations of a flag to be switched on like this:

======
switch -regexp -- $variable {
    -h(e(lp?)?)?             {...}
    -h|-he|-hel|-help|-ayuda {...}
    -\\? {...}                     ;$ Match -? flag
    {(?x) - | -h | -help} {do stuff} ;# extended RE syntax
}
======

without the -regexp, you would write:

======
switch -- $variable {
    -h - -he - -hel - -help {...}
}
======

----

[LV]: I really find using `-` as a separator between switch case alternatives to be counter-intuitive .
normally one uses `|` for that sort of thing.

[glennj]: It might be clearer if more whitespace is used:

======
switch -- $variable {
    -h    - 
    -he   - 
    -hel  - 
    -help {...}
}
======



** `-regexp` and backrefs **

[SB] 2003-05-21: When I use `switch -regexp`, Tcl already use regexp once to enter the proper clause. How can the match pattern be accessed from within the clause, if possible, and is switch `-regexp` able to use () match variables? I think perl store the regexp matches in global variables $1 .. $9 
(The following example is not valid, only for illustrating question)

======
switch -regexp -- $somevar {
    {^(\S+)\s+(\S+)} {puts "You hit matches $1 and $2"}
    {^(\d+)\s+(\d+)} {puts "You hit number matches $1 and $2"}
}
======

[KBK] - You can't.  However, that form of `switch` is no slower than the corresponding `[if] ... [elseif]`, 
so you can do:

======
if {[regexp {^(\S+)\s+(\S+)$} $somevar -> part1 part2]} {
     puts "You hit matches $part1 and $part2"
} elseif {[regexp {^(\d+)\s+(\d+)} -> part1 part2]} {
     puts "You hit number matches $part1 and $part2"
}
======

[hkoba] 2005-03-18: Hmm, how about [switch-regexp]?

[DKF]: With 8.5, you can tell `switch` to store the re-matched values instead of discarding them.



** Delimiting the Options **

[LES]: So, if it is so good a practice ALWAYS to call switch as "switch --" (the switch to end all switches), why can't the core code already cope with that?

[MG]: That's not really specific to switch - it's really good practice with ''all'' commands that take ''-something'' args, and show the end of those with '--', to include the --. And I think the reason the core can't handle is that sometimes you may actually want the behaviour that occurs when you don't give `--`.

[LV]: because technically the rule isn't "ALWAYS call switch as `switch --`".
The rule is "ALWAYS call switch with `--` after the last of the switch
flags".  If you are using switch without a flag, then it would be "`switch --`".
Think of code like this:

======
set arg {something entered by the user}
switch $arg {
    --first_thing {do something}
    --second_thing {do something else}
    default {do the last thing}
}
======

Now, if the user happens to type in `--first_thing`, the program
raises the error:

======none
bad option "--first_thing": must be -exact, -glob, -regexp, or --
======

because there was no `--` between the switch and the variable being
examined. Not only that, but if the user happens to type in `-exact`,
the error raised is:

======none
wrong # args: should be "switch ?switches? string pattern body ... ?default body?"
======

because of the missing `--`.  Two potential error situations - and
the core switch command can't handle either, because by the time it is
called, any information about where the first argument is already gone.

[RS]: The need for `--` seems to exist only for ''switch''. Other commands don't get confused by parameters that look like switches but aren't:

======none
% lsearch -foo -bar
-1
======

Then again, `[regexp]` has the same need for `--`:

======none
% regexp -foo -bar
bad switch "-foo": must be -all, -about, -indices, -inline, -expanded, -line, -linestop, -lineanchor, -nocase, -start, or --
======

Yax: `[lsearch]` assumes the form of arguments by the argument count, and its quite probable that all commands that take fixed args after options do. Any commands (such as regexp) that can take an arbitrarily varying number of args and have switches probably also suffer this issue.



** `[break]` **

[LV]: When I try to use a break inside a `switch` script, I get the error

======none
invoked "break" outside of a loop
    while executing
"switch 
======

Is there an equivalent to `[break]` for switch?

[YSK]: How about trapping `switch` in `[while] 1`:

======
while 1 {
    switch $cond {
        case1 {
           if {$fault} {puts NG; break}
           puts OK
        }
        default {
           puts OK
        }
    }
    break
}
======

[MG] Hrm, I thought `[break]` worked in switches, too. But it seems that Tcl's [switch] doesn't fall through by default:

======
switch -glob -- foo \
    f* {puts match1} \
    *o* {puts match2} \
    default {puts nomatch}
======

Only the "match1" is shown, and then it stops, there's no "match2". So no `[break]` is needed, it'll stop when a match is found anyway. That means, in something like

======
switch -glob -- $string \
    f* {puts foo}
    b* {puts bar}
    *z* {# do nothing}
    default {puts {something else}}
======

the "# do nothing" case has the same effect as a `[break]` in a switch in [C] would.

[LV] without `[break]`, it means that one has to code carefully to avoid the cases where one normally would use it. For instance, in other languages, one might code

======
switch $variable {
case1 {do this; break}
case2 {do that; break}
case3 {if today is Monday, do the other; break; if tomorrow is the 14th, do something else; break;}
default {if an error, say so and break; do the last thing, break;}
======

In Tcl, one would recode using `[if%|%else]` or `[if%|%elseif]` or fiddling
with state variables, etc. to accomplish the same thing.



** Using the Return Value of `switch` **

[RS] 2005-05-30: Unlike in [C], [Tcl]'s `[switch]` returns its last evaluated result, 
so it can be used as a function. Given this simple [identity function%|%identity]

======
proc is x {set x}
======

we can code the following "digit classifier" quite elegantly:

======
foreach num {0 1 2 3 4 5 6 7 8 9} {
    set type [switch -- $num {
        1 - 9         {is odd}
        2 - 3 - 5 - 7 {is prime}
        0 - 4 - 6 - 8 {is even}
    }]
    puts "$num is $type"
}
======

which displays

======none
0 is even
1 is odd
2 is prime
3 is prime
4 is even
5 is prime
6 is even
7 is prime
8 is even
9 is odd
======

[AMG]: See [identity function ] for more discussion of this pattern.  The simplest way to do it is with
single-argument `[lindex]`.


** History **

First announced by [JO] for Tcl 7.0 in [https://groups.google.com/d/msg/comp.lang.tcl/u1pATk0ytWM/8uStOsyBrLYJ%|%Advice wanted: "case" command and regexps], [comp.lang.tcl],1993-06-05



** Compilation Notes **

[DKF]: In Tcl [Changes in Tcl/Tk 8.5%|%8.5], `switch` is [bytecode%|%byte-compiled] if reasonably possible. The "reasonably possible" part means that you do have to be doing either '''`-exact`''' or '''`-glob`''' matching, and the bodies of the arms have to be compilable (as usual). With '''`-regexp`''' matching, that always goes to the "interpreted" version (though by compensation, there are some other new goodies (see `-matchvar` and `-indexvar`, for instance) available).

In most cases, the switch is compiled to (effectively) a sequence of `[if]` commands. However, in some special cases, i.e. '''`-exact`''' matching of constant terms, a jump table is built instead, which is considerably more efficient, especially for terms other than the first one.


** Bug: `-indexvar` Indexes Off By One **

[TJE]: Why are the ranges placed in a `switch` statement's `-indexvar` target inclusive of the character AFTER the match?  This differs from the behavior of regexp's `-indices` option, which seems quite odd to me.  For example:

======
% set line {foo bar}
foo bar
% regexp -inline -indices foo $line
{0 2}
% switch -regexp -indexvar index -- $line foo {set index}
{0 3}
======

Doesn't that seem odd?  'Tis not at all what I'd expected...

[PYK] 2014-05-30: If this really happened, it must have been a bug in some
version of Tcl. The current version behaves as expected, returnint `{0 2}`.



** Comments in Switch Statements **

Comments can be placed in the bodies of `switch`:

======
switch $x {
    1 {
        #comment goes here
        ...
    }
    2 {
        #comment goes here
        ...
    }
}
======

----
[Wookie]: I have a large switch and wanted comments for blocks of statements e.g.

======
switch -exact -- $test {
    # Comment for first block
    Test1 {do stuff...}
    Test2 {do stuff...}
  
    # Comment for second block
    Test3 {do stuff...}
    Test4 {do stuff...}
  
    # Comment for default
    NONE -
    default {SomeCleanUpProc...}
}
======

You get the error:
   "extra switch pattern with no body, this may be due to a comment incorrectly placed outside of a switch body - see the "switch" documentation"

As Tcl `switch` statements don't seem to care that there are multiple definitions of a statement, I rewrote the code as: 

======
switch -exact -- $test {
    COMMENT {# Comment for first block}
    Test1 {do stuff...}
    Test2 {do stuff...}
  
    COMMENT {# Comment for second block}
    Test3 {do stuff...}
    Test4 {do stuff...}
  
    COMMENT {# Comment for default}
    NONE -
    default {clean up stuff...}
}
======

If COMMENT is passed in it simply calls the first COMMENT in the switch, which does nothing. If COMMENT is a valid statement it would have to be dealt with in the first block.

[PYK] 2014-09-17:  A [scripted list] is another option.

[AMG]: Or use heinously awkward tricks with [list] and [{*}] [https://wiki.tcl-lang.org/page/Script+substitution#b2b62f3ef80d24075ccfb3b7184730e4f61679b734fd39ab1dd141154db50ff7].

======
switch -exact -- $test [list {*}[
    # Comment for first block
]   Test1 {do stuff...}\
    Test2 {do stuff...}\
{*}[
    # Comment for second block
]   Test3 {do stuff...}\
    Test4 {do stuff...}\
{*}[
    # Comment for default
]   NONE -\
    default {SomeCleanUpProc...}\
]
======

** Numeric Comparison **

[AMG]: Unlike [C]'s switch, which does numeric comparisons, [Tcl] switch performs `[string]` comparisons.  This can make a big difference in your program if you are dealing with numbers that are formatted in different ways.

======
set number 0x5
switch -- $number {
    5       {puts five!}
    default {puts unknown}
}
======

You may instead consider a long `[if]`/`[elseif]` chain, but maybe this won't [bytecode] as well.  Alternately, force the [format%|%formatting] to be consistent:

======
set number 0x5
switch -- [format %d $number] {
5       {puts five!}
default {puts unknown}
}
======



** Performance of `-regexp` **

[DrASK]:

Is there any speed improvement when using `-exact` over `-regexp`? As in:

======
switch -exact -- $value {
    a - b - c match
}

switch -regexp -- $value {
    [abc] match
}
======

[Lars H]: Comments by [DKF] above suggest that the answer should be yes, but why not check for yourself? `[time]` it!

Also note that the two [switch]es aren't exactly equivalent: the latter checks whether the $value ''contains'' a, b, or c, not whether it ''is''. To do that, you need to change the regexp pattern as follows:

======
switch -regexp -- $value {
   ^[abc]$ {match}
}
======

[DrASK]: Yeah, I failed to mention that `$value` is known to be a single character. Thanks. Here is the test and the results:

======
proc exact value {
    switch -exact -- $value {
        a - b - c {return true}
        default {return false}
    }
}

proc re value {
    switch -regexp -- $value {
        [abc] {return true}
        default {return false}
    }
}

proc first value {
    return [expr {[string first $value abc] != -1}]
}

proc multi {type value} {
    for {set i 0} {$i < 100} {incr i} {
        $type $value
    }
}

foreach type {exact re first} {
    foreach value {a b c d} {
        puts "$type $value [errortime {multi $type $value} 10000 100]"
    }
}
======
======none
exact a 66 +/- 0 microseconds per iteration
exact b 67 +/- 0 microseconds per iteration
exact c 67 +/- 0 microseconds per iteration
exact d 66 +/- 0 microseconds per iteration
regexp a 151 +/- 0 microseconds per iteration
regexp b 140 +/- 0 microseconds per iteration
regexp c 142 +/- 0 microseconds per iteration
regexp d 121 +/- 0 microseconds per iteration
first a 71 +/- 0 microseconds per iteration
first b 71 +/- 0 microseconds per iteration
first c 71 +/- 0 microseconds per iteration
first d 70 +/- 0 microseconds per iteration
======

It's interesting to note the dropoff when `[regexp]` hit the default case. But `-regexp` is clearly is 2-2.5x slower. I used [MAK]'s [How to Measure Performance%|%errortime proc] for timing.

[ferrieux]: Indeed, timing shows that the jump table is roughly 3x faster than `if..elseif..elseif`, which is itself 1.25x faster than `[regexp]`. Notice that the `[regexp]` compilation overhead is not in the picture, since it is cached. Not sure whether we should worry about the 1.25...


** An Implementation in Tcl **

[rwm]: I recently needed to run some [Changes in Tcl/Tk 8.5%|%8.5] code in [Changes in Tcl/Tk 8.4%|%8.4].  I wrote the following code to mimic the 8.5 switch in 8.4. (I looked for but could not find a compatibility library...) Note the many errors for cases that I did not need.  Maybe someone else will cover fix/post those ;)  Feel free to edit/move this as appropriate.

======
proc switch_8.5 args} {
    ## this is an 8.4 version of the 8.5 switch (incomplete)
    if {[llength $args] < 2} {
        error "usage: switch ?options? string {pattern body ?pattern body ...?}"
    }
    set type exact
    while {[string match -* [lindex $args 0]]} {
        set option [lindex $args 0]
        set args [lrange $args 1 end]
        switch -- $option {
            -exact    {set type exact}
            -nocase   {set type nocase}
            -glob     {set type glob}
            -regexp   {set type regexp}
            -matchvar {set matchvar [lindex $args 0]; set args [lrange $args 1 end]}
            -indexvar {error "unimplemented"}
            -- break
            default {error "bad switch option=$option"}
        }
    }
    set string [lindex $args 0]
    if {[llength $args] == 2} {
        set body [lindex $args 1]
    } else { 
        set body [lrange $args 1 end]
    }
    switch -- $type {
        exact  {error unimplemented}
        nocase {error unimplemented}
        glob   {error unimplemented}
        regexp {
            foreach {rexp script} $body {
                if {$rexp eq {default}} {
                    uplevel 1 $script
                    break
                }
                if {[regexp -- $rexp $string match 1 2 3 4 5 6 7 8 9]} {
                    if {[info exists matchvar]} {
                        upvar $matchvar m
                        set m [list $match $1 $2 $3 $4 $5 $6 $7 $8 $9]
                        uplevel 1 $script
                        unset m
                    } else {
                        uplevel 1 $script
                    }
                    break
                }
            }
        }
        default {error "internal type=$type error"}
    }
}
======



** A Dynamic `switch` **

[neb]: ddd asked about https://groups.google.com/d/msg/comp.lang.tcl/tsNZHbu_K0A/8yTwNFjgOu8J%|%an expandable switch statements%|%. I'm putting some of the answers here, because I intend to possibly use them =]

[Schelte Bron] said: The `switch` command (at least in the format you used) takes a list as its last argument. So you can simply put that into a variable and use the normal list operations to manipulate it:

======
set switch {
   a*b     -
   b       {expr 1}
   a*      {expr 2}
   default {expr 3}
}

switch -glob aaab $switch

set switch [linsert $switch end-2 c {expr 4}]

switch -glob c $switch
======

[DKF] said: I'd do it like this, using the alternate form of `[switch]`:

======
set defs {
    a*b     -
    b       {expr 1}
    a*      {expr 2}
}

switch -glob -- $value {*}$defs default {
    expr 3
}
======

Like that, adding another clause is just this:

======
lappend defs c {expr 4}
======

Yet you won't disrupt the special handling of `default`, which has to be the last clause. (You could also put the default handling script in its own variable, or other arbitrarily complex solution. Your call.) 


** A Misunderstanding **

Someone wrote:

There is a typo in the current switch.html documenation.  The original code looks like:

======
set foo abc
switch abc a - b {expr 1} $foo {expr 2} default {expr 3}
======

It should read:

======
switch abc {a - b} {expr 1} $foo {expr 2} default {expr 3}
======

2014-04-11: I don't think it's a typo, because it's valid, if a little contrived, code.  The original code means that if `abc` were equal to `a` ''or'' `"b"`, the result would be `1`. However, if you really want to match against the string `a - b`, then you need to enclose it in quotes or braces.



** Discussion **

[LV]: Can someone help me with switch? I have what seems like a simple
bit of code, but it doesn't work as I expected. So I need help adjusting
my expectation (and my code!).

======
#! /usr/tcl84/bin/tclsh

set ::expert no
set ::usage "USAGE: $argv0 arg1 arg2"
set ::val XYZ

foreach i $::argv {
    switch -exact -- $i in {
        ! {
            if {$::expert eq {no}} {
                set ::expert yes
            } else {
                puts $::usage
                exit 1
            }
        }
        1 {set $::val 1}
        default {
               puts {** Too many values specified **}
               puts {You will be prompted for the information **}
               set ::expert no
        }
    }
}

puts $::expert
puts $::val
$ ts.tcl 1 2 !
no
XYZ
======

I was expecting to see 

======none
no 
** Too many values specified **
You will be prompted for the information **
1
======

as a result, since I supplied a 2 and a !.

What am I missing in this?

[RHS]: There is an extraneous '''`in`''' there:

======
switch -exact -- $i in {
                    ^^
======



<<categories>> Command | Control Structure | Tcl syntax | Arts and crafts of Tcl-Tk programming