Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc logmsg {level args} {
    puts "\[$level\] [join $args " "]"
}

logmsg INFO starting up
logmsg WARN clock skew detected on domain two
logmsg ERROR failed
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[INFO] starting up
[WARN] clock skew detected on domain two
[ERROR] failed

level 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc describe {args} {
    return "[llength $args] argument(s): [join $args {, }]"
}

puts [describe]
puts [describe one]
puts [describe one two three]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0 argument(s): 
1 argument(s): one
3 argument(s): one, two, three

With 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 ...:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc logmsg {level args} { }
logmsg
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wrong # 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc inner {a b c} {
    return "a=$a b=$b c=$c"
}

proc forward_wrong {args} {
    return [inner $args]
}

forward_wrong 1 2 3
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wrong # 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc forward_right {args} {
    return [inner {*}$args]
}

puts [forward_right 1 2 3]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc mid {a args b} {
    return "a=$a args='$args' b=$b"
}
puts "defined without complaint"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
defined without complaint

No 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts [mid 1 2 3]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a=1 args='2' b=3

One value, not a collection. And the count is now fixed at three:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mid 1 2
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wrong # 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc joinall {args} {
    return [join $args "-"]
}
puts [joinall a b c]
puts [joinall]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a-b-c

3. $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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc wrap {args} {
    return [format {*}$args]
}
puts [wrap "%s-%s" a b]
puts [wrap "%05d" 42]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a-b
00042

6. 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 args collects every remaining argument into a list. The name is reserved for this.
  • With nothing left over, args is the empty list — no special case needed.
  • Named parameters before args are still required; the usage line shows a trailing ....
  • {*}$args forwards the collected arguments as separate words; plain $args passes 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.
  • args is 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 args compete for the same positions — prefer an options dictionary when you need both.

Where this fits

Part of the Tcl curriculum.