Version 18 of Scripted List

Updated 2015-10-23 13:07:15 by pooryorick

A scripted list is a Tcl script in which every word of every command becomes a value in the resulting list, with the first word of each command being considered just another value instead of being used as the command name.

See Also

scripted templates
a twist on scripted lists
Convenient list arguments - larg
a similar idea from jcw

Download

In addition to the full implementation below, sl is also available as ycl list sl.

Description

One way to look at a Tcl script is as a sequence of words that are grouped into commands. The sl command presented below extracts those words into a single list while obeying the semantics of a Tcl script. Representing a list in this way, as a Tcl script, allows for elements on multiple lines without having to escape the newline, and also allows for comments between words in the list.

For example a switch can contain comments between its blocks:

set grail 42
switch $answer [sl {
    #set course for Traal
    {a lurgid bee} {
        lindex {Do not be alarmed} 
    }
    #By Agrajag!
    $grail {
        lindex {Oh no, not again.} 
    }
    #clueless Golgafrinchams 
    default {
        puts [list whoa $answer]
        error {get your cave redecorated!}
    }
}]

the following procedure provides the described behaviour:

proc sl script {
    concat {*}[lmap part [scriptSplit $script] {
        if {[string match #* $part]} continue
        uplevel 1 list $part
    }]
}

Another use for a scripted list is in parsing data formatted as a Tcl script:

proc configure script {
    foreach command [cmdSplit $script] {
        set command [namespace eval configns [
            list [namespace which sl] $script]]
        set cmdargs [lassign $command name]
        switch $name {
            someparameter {
                do something with $cmdargs
            }
            someotherparameter {
                do something with $cmdargs
            }
        }
    }
}

A Twisted Scripted List

This list is a twist on the standard scripted list in which only the first command in a series of semicolon-separated commands becomes part of the result. The additional commands in such a series of commands might have side effects that influence the composition of the list. The current examples don't show a compelling use case, but perhaps someone will do something interesting with it.

Example

set mylist [sl {
    one
    two three  ;#this is a comment
    $somevar
    #ha ha! a two-line \
    comment in the list!
    [some command]
    {;} ;#literal semicolons must be escaped
    {some literal}
    "some $substituted[value]"
}]

More Complex Example

set six six
proc some {args} {
    return {seven eight}
}
proc value {} {
    return {nine ten}
}

proc side {args} {
    upvar var2 var2
    set var2 twelve 
}
set substituted eleven
set mylist [sl {
    one
    two three  ;#this is a comment
    {four five}
    $six
    #this is a a two-line \
    comment in the list!
    [some command]
    {;} ;#literal semicolons must be escaped
    "[value] $substituted"

    only the first command in a line get the special scripted list treatment, \
    but then only only the value othe last command in a line counts which \
    means that this line is like a comment with [side effects], and its \
    result is whatever the last command produces; set var2 
}]

puts $mylist

Output:

one two three {four five} six {seven eight} {;} {nine ten eleven} twelve

Implementation

proc sl script {
    set res {}
    set parts {}
    foreach part [split $script \n] {
        lappend parts $part
        set part [join $parts \n]
        #add the newline that was stripped because it can make a difference
        if {[info complete $part\n]} {
            set parts {}
            set part [string trim $part]
            if {$part eq {}} {
                continue
            }
            if {[string index $part 0] eq {#}} {
                continue
            }
            #Here, the double-substitution via uplevel is intended!
            lappend res {*}[uplevel list $part]
        }
    }
    if {$parts ne {}} {
        error [list {incomplete parts} [join $parts]]
    }
    return $res
}

History

The following was migrated here from Additional string functions:

Substitution that Preserves Grouping

RHS 2005-02-23: There have been a number of times where I have wanted a version of subst that preserves grouping. For example, I want to be able to do the following:

% array set myarr [subst {
    a $x
    b [getConfigValue something]
    c {
        a b
        c d
    }
}]
% array get myArr
a 1 b blah c {
        a b
        c d
    }

My main reason for this is, when the dataset get large, I tend to find \ line continuations fairly ugly... That, and my emacs config doesn't indent well for multiple levels of line continuations if there's sub levels.

In pursuit of the above, I came up with the following:

proc gsubst input {
    set data {}
    foreach line [split $input \n] {
        if {[info complete $data]} {
            append data " $line"
        } else {
            append data "\n$line"
        }
    }
    uplevel 1 list $data
}

The above proc should, in theory, cause the following two to work exactly the same

list a 1 \
    b $x \
    c {
        1
        2
    } \
    d [somecommand]
gsubst {
    a 1
    b $x
    c {
        1
        2
    }
    d [somecommand]
}

PYK 2015-10-23: Because it's using a less robust implementation of scriptSplit, gsubst chokes on semicolon-delimited commands and does not ignore comments. Also, to properly split a script into commands, it should be info complete $data\n rather than info complete $data but the false positive doesn't cause any problems because the newlines that delimit commands are just getting filtered out anyway.

Page Authors

RHS
PYK
aspect