Skip to content

Commit 4255d54

Browse files
committed
ensemble: Add a simple ensemble command
Uses a prefix to automatically map from subcommand to implementation. Includes support for namespace ensemble Signed-off-by: Steve Bennett <[email protected]>
1 parent d295fb1 commit 4255d54

File tree

7 files changed

+225
-2
lines changed

7 files changed

+225
-2
lines changed

README.ensemble

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
An ensemble is a single command that can dispatch subcommands
2+
to other commands.
3+
4+
For example [string] is a built-in ensemble.
5+
6+
The ensemble command allows an ensemble command to be created
7+
that redirects to other commands.
8+
9+
Create an ensemble by having multiple commands that all share
10+
the same prefix. For example:
11+
12+
proc {test open} {name} { ... }
13+
proc {test close} {handle} { ... }
14+
proc {test show} {handle} { ... }
15+
16+
Then simply:
17+
18+
ensemble test
19+
20+
Now a new command, test, is created that will invoke the other commands
21+
based on the first argument. For example:
22+
23+
set h [test open file.txt]
24+
test show $h
25+
test close $h
26+
27+
By default ensemble expects the commands to be named "<name> ". If another
28+
prefix is used, this can be specified with the -automap option. e.g.
29+
30+
ensemble test -automap test.
31+
32+
This could be used if the commands were named test.open, test.close, test.show
33+
34+
Note that ensembles are dynamic, not fixed at the point of creation.
35+
This means, for example, that we can can create a new commands, "test reverse"
36+
after the ensemble has been created and it can still be invoked as test reverse ...
37+
38+
It is easy to create an ensemble for commands in a namespace by simply using
39+
-automap <ns>:: however for compatibility with Tcl, 'namespace ensemble create' is provided
40+
that does with when invoked within a namespace. e.g.
41+
42+
namespace eval test {
43+
namespace ensemble create
44+
45+
proc open {name} { ... }
46+
proc close {handle} { ... }
47+
proc show {handle} { ... }
48+
}
49+
50+
test open file.txt

auto.def

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ foreach {mod attrs help} {
6565
array {} {Tcl-compatible array command}
6666
binary { tcl optional } {Tcl-compatible binary command}
6767
clock {} {Tcl-compatible clock command}
68+
ensemble { optional tcl } {Ensemble command}
6869
eventloop { static } {after, vwait, update}
6970
exec { static } {Tcl-compatible exec command}
7071
file {} {Tcl-compatible file command}

ensemble.tcl

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Implement the ensemble command
2+
3+
proc ensemble {command args} {
4+
set autoprefix "$command "
5+
set badopts "should be \"ensemble command ?-automap prefix?\""
6+
if {[llength $args] % 2 != 0} {
7+
return -code error "wrong # args: $badopts"
8+
}
9+
foreach {opt value} $args {
10+
switch -- $opt {
11+
-automap { set autoprefix $value }
12+
default { return -code error "wrong # args: $badopts" }
13+
}
14+
}
15+
proc $command {subcmd args} {autoprefix {mapping {}}} {
16+
if {![dict exists $mapping $subcmd]} {
17+
# Not an exact match, so check for specials, then lookup normally
18+
if {$subcmd in {-commands -help}} {
19+
# Need to remove $autoprefix from the front of these
20+
set prefixlen [string length $autoprefix]
21+
set subcmds [lmap p [lsort [info commands $autoprefix*]] {
22+
string range $p $prefixlen end
23+
}]
24+
if {$subcmd eq "-commands"} {
25+
return $subcmds
26+
}
27+
set command [lindex [info level 0] 0]
28+
return "Usage: \"$command command ... \", where command is one of: [join $subcmds ", "]"
29+
}
30+
# cache the mapping
31+
dict set mapping $subcmd ${autoprefix}$subcmd
32+
}
33+
# tailcall here we don't add an extra stack frame, e.g. for uplevel
34+
tailcall [dict get $mapping $subcmd] {*}$args
35+
}
36+
}

jim-namespace.c

+2-2
Original file line numberDiff line numberDiff line change
@@ -201,13 +201,13 @@ static int JimNamespaceCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
201201
int option;
202202
static const char * const options[] = {
203203
"eval", "current", "canonical", "qualifiers", "parent", "tail", "delete",
204-
"origin", "code", "inscope", "import", "export",
204+
"origin", "code", "inscope", "ensemble", "import", "export",
205205
"which", "upvar", NULL
206206
};
207207
enum
208208
{
209209
OPT_EVAL, OPT_CURRENT, OPT_CANONICAL, OPT_QUALIFIERS, OPT_PARENT, OPT_TAIL, OPT_DELETE,
210-
OPT_ORIGIN, OPT_CODE, OPT_INSCOPE, OPT_IMPORT, OPT_EXPORT,
210+
OPT_ORIGIN, OPT_CODE, OPT_INSCOPE, OPT_ENSEMBLE, OPT_IMPORT, OPT_EXPORT,
211211
OPT_WHICH, OPT_UPVAR,
212212
};
213213

nshelper.tcl

+14
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,17 @@ proc {namespace upvar} {ns args} {
143143
}
144144
tailcall {*}$script
145145
}
146+
147+
proc {namespace ensemble} {subcommand args} {
148+
if {$subcommand ne "create"} {
149+
return -code error "only \[namespace ensemble create\] is supported"
150+
}
151+
set ns [uplevel 1 namespace canon]
152+
set cmd $ns
153+
if {$ns eq ""} {
154+
return -code error "namespace ensemble create: must be called within a namespace"
155+
}
156+
157+
# Create the mapping
158+
ensemble $cmd -automap ${ns}:: {*}$args
159+
}

tests/ensemble.test

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
source [file dirname [info script]]/testing.tcl
2+
3+
needs constraint jim
4+
needs package ensemble
5+
6+
# Let's create some procs for our ensembles
7+
8+
proc {foo a} {x} {
9+
incr x
10+
}
11+
proc {foo b} {y} {
12+
incr y 2
13+
}
14+
test ensemble-1.1 {Basic ensemble} {
15+
ensemble foo
16+
foo a 5
17+
} 6
18+
19+
test ensemble-1.2 {ensemble -commands} {
20+
foo -commands
21+
} {a b}
22+
23+
test ensemble-1.3 {ensemble -help} {
24+
foo -help
25+
} {Usage: "foo command ... ", where command is one of: a, b}
26+
27+
test ensemble-1.4 {ensemble with invalid subcommand} -body {
28+
foo c x
29+
} -returnCodes error -result {invalid command name "foo c"}
30+
31+
test ensemble-1.5 {ensemble add new commands} {
32+
proc {foo c} {z} {
33+
append z @
34+
}
35+
foo c x
36+
} {x@}
37+
38+
test ensemble-1.6 {ensemble remove mapping} -body {
39+
rename {foo a} ""
40+
foo a 4
41+
} -returnCodes error -result {invalid command name "foo a"}
42+
43+
test ensemble-1.7 {ensemble updated -commands} {
44+
foo -commands
45+
} {b c}
46+
47+
testreport

tests/nsensemble.test

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
source [file dirname [info script]]/testing.tcl
2+
3+
needs constraint jim
4+
needs cmd ensemble
5+
needs cmd namespace
6+
testConstraint package-ensemble [expr {"ensemble" in [package list]}]
7+
8+
# Let's create some procs for our ensemble
9+
namespace eval foo {
10+
proc a {x} {
11+
incr x
12+
}
13+
proc b {y} {
14+
incr y 2
15+
}
16+
proc c {z} {
17+
append z @
18+
}
19+
}
20+
21+
test nsensemble-1.0 {Create ensemble outside namespace} -body {
22+
# Create an ensemble for our namespace
23+
namespace ensemble create
24+
} -returnCodes error -result {namespace ensemble create: must be called within a namespace}
25+
26+
test nsensemble-1.1 {Basic namespace ensemble} {
27+
# Create an ensemble for our namespace
28+
namespace eval foo {
29+
namespace ensemble create
30+
}
31+
# And invoke a method
32+
foo a 5
33+
} 6
34+
35+
test nsensemble-1.2 {namespace ensemble -commands} package-ensemble {
36+
foo -commands
37+
} {a b c}
38+
39+
test nsensemble-1.3 {namespace ensemble -help} package-ensemble {
40+
foo -help
41+
} {Usage: "foo command ... ", where command is one of: a, b, c}
42+
43+
test nsensemble-1.4 {namespace ensemble with invalid subcommand} -constraints package-ensemble -body {
44+
foo d x
45+
} -returnCodes error -result {invalid command name "foo::d"}
46+
47+
# Now a nested namespace ensemble
48+
namespace eval foo {
49+
namespace eval bar {
50+
proc a {x} {
51+
incr x 10
52+
}
53+
proc b {y} {
54+
incr y 20
55+
}
56+
proc c {z} {
57+
append z %
58+
}
59+
namespace ensemble create
60+
}
61+
}
62+
63+
test nsensemble-2.1 {Nested namespace ensemble} {
64+
# And invoke a method
65+
foo::bar a 5
66+
} 15
67+
68+
test nsensemble-2.2 {Nested namespace ensemble from namespace} {
69+
# And invoke a method
70+
namespace eval foo {
71+
bar a 6
72+
}
73+
} 16
74+
75+
testreport

0 commit comments

Comments
 (0)