Tcl Part I · Module 10 · Procedures and Reuse
args: Taking Any Number of Arguments
Some procedures cannot know in advance how many arguments they will get — a logging call that takes a message of any length, a wrapper that must pass whatever it was given to something else. Tcl handles this with a single reserved parameter name, and the mechanism is pleasingly small: name your last parameter args and it collects everything that is left into an ordinary list. What makes it powerful is the other half, which is how you hand that list on to another command as separate arguments rather than as one. That pairing is the basis of every wrapper in Tcl, and it has one trap that this chapter demonstrates rather than describes.
Foundation11 min readTclPart IProceduresargsWrappers
Part I · Module 10 · Chapter 10.4 · args: Taking Any Number of Arguments
1. The last parameter can be args
proc logmsg {level args} {
puts "\[$level\] [join $args " "]"
}
logmsg INFO starting up
logmsg WARN clock skew detected on domain two
logmsg ERROR failed[INFO] starting up
[WARN] clock skew detected on domain two
[ERROR] failedlevel takes the first argument; everything after it is collected into args as a list. The name is reserved — Tcl treats a final parameter called args specially and no other name does this.
2. It is an ordinary list
Nothing exotic arrives in args. Every list command from Module 6 applies:
proc describe {args} {
return "[llength $args] argument(s): [join $args {, }]"
}
puts [describe]
puts [describe one]
puts [describe one two three]0 argument(s):
1 argument(s): one
3 argument(s): one, two, threeWith nothing left over, args is the empty list — not an error and not an empty string you have to special-case. A procedure whose only parameter is args therefore accepts any number of arguments including none.
3. The usage line
From Chapter 10.2 §4, a variadic procedure advertises itself with a trailing ...:
proc logmsg {level args} { }
logmsgwrong # args: should be "logmsg level ..."So level is still required — args collects what is left, and does not excuse you from the named parameters before it.
4. Forwarding with the expansion operator
This is what makes args more than a convenience. A list passed as one argument stays one argument:
proc inner {a b c} {
return "a=$a b=$b c=$c"
}
proc forward_wrong {args} {
return [inner $args]
}
forward_wrong 1 2 3wrong # args: should be "inner a b c"$args was the single value 1 2 3, so inner received one argument instead of three. The expansion operator from Chapter 6.13 splits it back into separate words:
proc forward_right {args} {
return [inner {*}$args]
}
puts [forward_right 1 2 3]a=1 b=2 c=3{*}$args is the forwarding idiom. You will see it in every wrapper you ever read.
5. args is only special in the last position
Here is the trap, and it has the same shape as Chapter 10.3 §5's: Tcl accepts a parameter list that does not mean what it looks like.
proc mid {a args b} {
return "a=$a args='$args' b=$b"
}
puts "defined without complaint"defined without complaintNo error. But args is not in the last position, so it is not special — it is an ordinary parameter that happens to be called args, taking exactly one value:
puts [mid 1 2 3]a=1 args='2' b=3One value, not a collection. And the count is now fixed at three:
mid 1 2wrong # args: should be "mid a args b"Note the usage line: no ..., because there is nothing variadic about this procedure. Tcl is telling you the truth; it just never warned you that the name you chose was doing nothing.
6. Combining with named and defaulted parameters
Positional arguments fill the named parameters first, in order, and whatever remains goes to args:
proc both {a {b 2} args} {
return "a=$a b=$b args='$args'"
}
puts [both 1]
puts [both 1 9]
puts [both 1 9 x y]a=1 b=2 args=''
a=1 b=9 args=''
a=1 b=9 args='x y'A second argument can never reach args while b is unfilled. That is worth knowing before you combine optional parameters with args, because it means the caller cannot omit b and still pass extras — the two features compete for the same positions.
7. A worked example
A logging wrapper that adds a prefix and forwards everything else:
proc emit {args} {
return "emit got [llength $args]: [join $args {|}]"
}
proc timestamped {level args} {
if {[llength $args] == 0} {
return [emit "\[$level\]" "(no message)"]
}
return [emit "\[$level\]" {*}$args]
}
puts [timestamped INFO starting]
puts [timestamped WARN two words here]
puts [timestamped ERROR]emit got 2: [INFO]|starting
emit got 4: [WARN]|two|words|here
emit got 2: [ERROR]|(no message)Three things are visible: the forwarded arguments stayed separate, the empty case was handled explicitly rather than producing a trailing separator, and timestamped never needed to know how many arguments emit takes.
8. Exercises
Exercise 1. What does args contain when a procedure declared {level args} is called as logmsg INFO?
Exercise 2. Write joinall taking any number of arguments and returning them joined with a hyphen.
Exercise 3. Why does inner $args fail where inner {*}$args works?
Exercise 4. What does this print, and why is it not variadic?
proc mid {a args b} { return "args='$args'" }
puts [mid 1 2 3]Exercise 5. Write wrap that calls format with whatever arguments it is given, so that wrap "%s-%s" a b returns a-b.
Exercise 6. A procedure is declared {a {b 2} args}. Can a caller supply something for args while leaving b at its default? Explain.
Answers
1. The empty list. llength $args is 0, and join $args " " produces the empty string.
2.
proc joinall {args} {
return [join $args "-"]
}
puts [joinall a b c]
puts [joinall]a-b-c3. $args substitutes as a single word — one argument whose value happens to be a list — so inner receives one argument and reports wrong # args. {*}$args expands the list into separate words, so inner receives three.
4. It prints args='2'. args is special only as the final parameter; here it is an ordinary parameter taking exactly one value, so the procedure requires exactly three arguments and nothing is collected.
5.
proc wrap {args} {
return [format {*}$args]
}
puts [wrap "%s-%s" a b]
puts [wrap "%05d" 42]a-b
000426. No. Arguments bind by position, so the second argument always fills b before anything reaches args. To leave b at its default the caller must pass only one argument — and then there is nothing left for args either.
9. Remember
- A final parameter named
argscollects every remaining argument into a list. The name is reserved for this. - With nothing left over,
argsis the empty list — no special case needed. - Named parameters before
argsare still required; the usage line shows a trailing.... {*}$argsforwards the collected arguments as separate words; plain$argspasses one argument that happens to be a list.- That pairing is the wrapper idiom — it lets a wrapper work without knowing its target's signature.
argsis only special in the last position. Anywhere else it is an ordinary parameter, silently, and the procedure is not variadic.- A missing
...in the usage line is how you spot that. - Defaulted parameters and
argscompete for the same positions — prefer an options dictionary when you need both.
Where this fits
Part of the Tcl curriculum.
