# Tcl to Perl You know Tcl. You write `set x 5`, you substitute with `$x`, you reach for `[expr {...}]` to do arithmetic, you build lists with `lappend` and walk them with `foreach`, and you bend strings with `regexp` and `regsub`. This guide maps each of those habits onto Perl, so you can keep every reflex you have and pick up the things Tcl makes hard. Tcl and Perl share the same roots: Unix scripting, a string-first worldview, and `$`-substitution as the way you reach a variable. Most of what you know maps almost directly - and Perl gives you real data structures (`@arrays` and `%hashes` as first-class types, not lists pressed into service) and a vast module ecosystem to land in. Two specific pieces of friction get the weight in this guide: Perl's distinct array/hash types where Tcl has lists and assoc arrays, and Perl's regex (a superset of Tcl's). The line-at-a-time and one-liner material is lighter here, because Tcl is not a one-liner language; you will spend your time in variables, data, control flow, and regex. Every runnable example here was executed on `pperl`, a Rust reimplementation of Perl 5.42; the `# ...` comments show its real output. Start scripts with: ```perl use v5.42; ``` That one line turns on `strict`, `warnings`, and the `say` built-in (a `print` with a trailing newline, the rough twin of Tcl's `puts`), the modern feature set used throughout. :::{note} One-liner switches on this pperl build Perl's one-liner switches - `perl -ne`, `perl -pe`, `-a`, `-i`, `-l` - are **correct perl5 5.42** and are the idiomatic form for ad-hoc text work. But the implicit-loop switch family (`-n`, `-p`, `-a`, `-F`, `-i`, `-l`) is **not yet recognised by this `pperl` build** - only `-e` is. So the one-liner examples below are shown *without* output, and each is paired with an explicit-loop full-script form (`while (<$fh>) { ... }`) that **does** run on `pperl` today and carries the verified output. Use the full-script form until the switches land. ::: ## How to read this - [Variables and substitution](#variables-and-substitution) - `set` becomes `my`, `[expr]` disappears, `$x` substitution stays. - [Lists and arrays](#lists-and-arrays) - `lindex`/`lappend`/`lsort` become Perl array operations. - [Assoc arrays and hashes](#assoc-arrays-and-hashes) - `set a(k) v` becomes `$a{k}`; `dict` becomes a hash. - [Procs and references](#procs-and-references) - `proc` becomes `sub`; `upvar`/`uplevel` become references. - [Regex and substitution](#regex-and-substitution) - `regexp`/ `regsub`/`string map` become `m//`/`s///`/`tr///`. - [Control flow](#control-flow) - `if`/`while`/`foreach`/`switch` become Perl's, plus `{}` vs `""` quoting. - [The line/record loop](#the-linerecord-loop) - reading a file line by line with `$_` and `$.`. - [Fields and splitting](#fields-and-splitting) - `split`/`join`, the Tcl `split` analogue. - [I/O, pipes, and in-place edit](#io-pipes-and-in-place-edit) - `open`/`gets`/`puts`/`exec` become `open`/`<$fh>`/`print`/`qx`. - [One-liners and switches](#one-liners-and-switches) - `-n`, `-p`, `-e`, kept brief. - [Gotchas coming from Tcl](#gotchas-coming-from-tcl) - scan before you write. ## Variables and substitution In Tcl, `set x 5` creates a variable and `$x` substitutes its value. In Perl, `my $x = 5;` declares a lexically scoped variable and `$x` substitutes it - the substitution syntax you already know carries over verbatim. The relief is that Perl has native operators: there is no `[expr {...}]`. Arithmetic is just arithmetic. ```tcl set x 5 set name "world" set sum [expr {$x + 3}] puts "x is $x" puts "sum is $sum" ``` ```perl use v5.42; my $x = 5; # set x 5 my $name = "world"; my $sum = $x + 3; # native operators, no [expr] needed say "x is $x"; # x is 5 say "sum is $sum"; # sum is 8 ``` Quoting maps cleanly, and this is a happy mapping: Tcl's `"..."` (substitution happens) is Perl's double-quoted string, and Tcl's `{...}` (literal, no substitution) is Perl's single-quoted string. ```perl use v5.42; my $name = "world"; say "hello, $name"; # hello, world (interpolates) say 'no $interp here'; # no $interp here (literal) ``` The gotcha: Tcl is "everything is a string" and Perl is close to it - a scalar auto-coerces between number and string by context - but Perl splits *comparison* into two operator families, and you pick by the operation, not the value. Numbers use `==`, `!=`, `<`, `>`; strings use `eq`, `ne`, `lt`, `gt`. ```perl use v5.42; my $x = "5"; say $x + 3; # 8 (numeric context coerces "5") say $x . "3"; # 53 (string context) say "10" == "10.0" ? "num-equal" : "num-differ"; # num-equal say "10" eq "10.0" ? "str-equal" : "str-differ"; # str-differ ``` `==` compares `"10"` and `"10.0"` as numbers (equal); `eq` compares them as strings (different). Using `==` where you meant `eq`, or the reverse, is the one quiet bug the string-first worldview sets you up for. ## Lists and arrays A Tcl list is a Perl array, and the verbs line up one for one. The big shift: in Tcl *everything is a list*, including strings you split on the fly; in Perl an array (`@a`) is a distinct, first-class type with its own sigil. When you reach for one element, the sigil follows the *element*, so it is `$a[0]` (a scalar), not `@a[0]`. ```tcl set items [list apple pear fig] puts [lindex $items 0] puts [llength $items] lappend items plum set sorted [lsort $items] foreach it $items { puts "item: $it" } ``` ```perl use v5.42; my @items = ("apple", "pear", "fig"); say $items[0]; # apple (lindex $items 0) say scalar @items; # 3 (llength $items) push @items, "plum"; # lappend items plum my @sorted = sort @items; # lsort say "@sorted"; # apple fig pear plum for my $it (@items) { # foreach it $items say "item: $it"; } ``` `lsearch` - "find the index of a matching element" - has no single built-in; the idiomatic Perl is `grep` over the index range, with `$#items` as the last index: ```perl use v5.42; my @items = ("apple", "pear", "fig", "plum"); my ($idx) = grep { $items[$_] eq "fig" } 0 .. $#items; # lsearch say "fig at $idx"; # fig at 2 ``` The gotcha: `scalar @items` is how you get the length (Tcl's `llength`); writing `my $n = @items` works because assigning an array to a scalar yields its count, but spell it `scalar @items` when you want the length inside a larger expression. And `sort` compares as *strings* by default, so `sort (10, 9, 100)` gives `10 100 9`; pass a numeric comparator `sort { $a <=> $b } @nums` for numeric order, where `$a` and `$b` are the two elements and `<=>` is the numeric three-way operator. ## Assoc arrays and hashes A Tcl associative array - `set a(red) 1` - is a Perl hash. The element access maps directly: Tcl's `$a(red)` is Perl's `$a{red}`, and the whole collection is `%a` (Tcl has no syntax for the array as a single value; Perl does). Modern Tcl's `dict` maps to the same Perl hash, or to a hash reference when you need to nest or pass it around. ```tcl set a(red) 1 set a(green) 2 set a(blue) 3 puts $a(red) puts [array exists a] foreach k [lsort [array names a]] { puts "$k => $a($k)" } unset a(blue) ``` ```perl use v5.42; my %a = (red => 1, green => 2, blue => 3); say $a{red}; # 1 ($a(red)) say exists $a{green} ? "yes" : "no"; # yes (info exists a(green)) for my $k (sort keys %a) { # array names a say "$k => $a{$k}"; } delete $a{blue}; # unset a(blue) ``` That prints: ```text 1 yes red => 1 green => 2 blue => 3 ``` The gotcha: membership is `exists $a{key}`, not a separate command, and the fat comma `=>` is the idiomatic key/value separator in a hash literal. `=>` also auto-quotes the bareword on its left, so `red => 1` needs no quotes around `red`. A hash has no inherent order (neither does a Tcl array); iterate `sort keys %a` when you want a stable one, exactly as you reach for `lsort [array names a]`. ## Procs and references `proc name {args} {body}` becomes Perl's `sub name { ... }`. The classic Tcl way to read arguments - positional, by name - has a direct Perl twin: `my ($a, $b) = @_;` unpacks the argument array `@_`. Modern Perl also has signatures, which read like Tcl's `{args}` list and add defaults. ```tcl proc add {a b} { return [expr {$a + $b}] } puts [add 3 4] proc greet {who {greeting Hello}} { return "$greeting, $who!" } puts [greet Tcl] puts [greet Perl Hi] ``` ```perl use v5.42; sub add { my ($a, $b) = @_; # unpack the argument array return $a + $b; } say add(3, 4); # 7 sub greet ($who, $greeting = "Hello") { # signature with default return "$greeting, $who!"; } say greet("Tcl"); # Hello, Tcl! say greet("Perl", "Hi"); # Hi, Perl! ``` Tcl's pass-by-name - `upvar`, where a proc reaches up and mutates a caller's variable - becomes a Perl **reference**: take one with `\`, follow it with `$$ref`. You pass the reference explicitly instead of naming the variable, which makes the data flow visible at the call site. ```perl use v5.42; sub increment { my ($ref) = @_; $$ref++; # mutate through the reference (Tcl: upvar) } my $count = 10; increment(\$count); # pass a reference to $count say $count; # 11 ``` The gotcha: `@_` is the argument array, and `my ($a, $b) = @_;` *copies* the arguments into named locals - the line you almost always want at the top of a sub. Reaching for `upvar`-style aliasing is rarely needed in Perl; when you do need to mutate a caller's data, pass a reference (`\$x`, `\@arr`, `\%h`) deliberately rather than aliasing by name. References are also how Perl nests data (an array of hashes, a hash of arrays); the same mechanism that replaces `upvar` is the one that replaces Tcl's string-encoded nested lists. ## Regex and substitution This is a payload section. Tcl's `regexp` and `regsub` use Advanced Regular Expressions; Perl's regex is a **superset**, so the patterns you know carry over and gain features. Perl makes regex *syntax*, not a command: `=~` binds a pattern to a string, `m//` matches, `s///` substitutes, and `tr///` transliterates. ```tcl set s "order 42 shipped" if {[regexp {(\d+)} $s -> num]} { puts "number: $num" } set t [regsub -all {o} "foo boo zoo" O] puts $t set u [string map {a A b B} "abcabc"] puts $u ``` ```perl use v5.42; my $s = "order 42 shipped"; if ($s =~ /(\d+)/) { # regexp; capture lands in $1 say "number: $1"; # number: 42 } (my $t = "foo boo zoo") =~ s/o/O/g; # regsub -all -> s///g say $t; # fOO bOO zOO (my $u = "abcabc") =~ tr/ab/AB/; # string map (char-for-char) say $u; # ABcABc ``` The capture maps too: Tcl's `-> num` match variable becomes Perl's `$1`, `$2`, and so on, with named captures landing in `%+`: ```perl use v5.42; if ("2026-06-21" =~ /(?\d{4})-(?\d{2})/) { say "$+{y} / $+{m}"; # 2026 / 06 } ``` Tcl's `string match` is glob matching, not regex (`string match *.txt $name`). In Perl, anchor a regex instead - `/\.txt$/` is the equivalent - because Perl's one pattern language covers both jobs. ```perl use v5.42; my $name = "report.txt"; say "matches" if $name =~ /\.txt$/; # string match *.txt $name # matches ``` The gotcha: `s///` modifies its target *in place* and returns the *count* of substitutions, not the new string - which is why the examples above copy first with `(my $t = "...")` before substituting. To get a new string and leave the original untouched, use the `/r` flag: `my $new = $s =~ s/o/O/gr`. And `tr///` (the `string map` analogue for single characters) is **not** a regex: its left side is a literal character set, so `tr/ab/AB/` maps `a` and `b` only. For pattern work, reach for `s///`. ## Control flow The control structures map almost one to one; the spelling shifts and the braces stay. Tcl's `if {cond} {body}` becomes Perl's `if (cond) { body }`, `elseif` becomes `elsif`, and `foreach` becomes `for` (the two spellings are aliases in Perl; `foreach` works too). ```tcl if {$n < 0} { set sign neg } elseif {$n == 0} { set sign zero } else { set sign pos } set i 0 while {$i < 3} { puts "i=$i"; incr i } foreach w {a b c} { puts "w=$w" } ``` ```perl use v5.42; my $n = 0; my $sign; if ($n < 0) { $sign = "neg" } elsif ($n == 0) { $sign = "zero" } # elseif -> elsif else { $sign = "pos" } say $sign; # zero my $i = 0; while ($i < 3) { say "i=$i"; $i++ } # incr i -> $i++ for my $w (qw(a b c)) { say "w=$w" } # foreach w {a b c} ``` Tcl's `switch` has no single keyword in Perl; the idiomatic replacement is a **dispatch hash** mapping each case to a code reference, which is faster and more flexible than a chain of `if`: ```perl use v5.42; my $cmd = "start"; my %dispatch = ( start => sub { "starting" }, stop => sub { "stopping" }, ); say $dispatch{$cmd}->(); # starting ``` The gotcha: the `{}` vs `""` distinction you rely on in Tcl for deferring versus substituting maps to Perl's `''` vs `""` for strings (covered above), but Perl's *block* braces are not quoting - a Perl `{ ... }` after `if`/`while`/`for` is a code block, always evaluated, never a deferred string. There is no "the body is a string until I run it" step; Perl parses the block at compile time. Postfix modifiers are a bonus Tcl has no form for: `say $_ for @items;` and `say "neg" if $n < 0;` carry the loop or condition after the statement. ## The line/record loop Tcl reads a file with `set fh [open f r]; while {[gets $fh line] >= 0} {...}`. Perl's equivalent is a `while (<$fh>)` loop, with each line landing in `$_` (the implicit topic variable) or in a named scalar. The line number is in `$.`. ```tcl set fh [open /tmp/tcl_data.txt r] while {[gets $fh line] >= 0} { puts "got: $line" } close $fh ``` ```perl use v5.42; open my $fh, '<', '/tmp/tcl_data.txt' or die "open: $!"; while (my $line = <$fh>) { # gets $fh line chomp $line; # <$fh> keeps the newline; strip it say "got: $line"; # puts } close $fh; # got: apple # got: banana # got: cherry ``` (The input file held `apple`, `banana`, `cherry`.) Using `$_` and the line counter `$.` reads even closer to a record loop: ```perl use v5.42; open my $fh, '<', '/tmp/tcl_data.txt' or die "open: $!"; while (<$fh>) { # each line lands in $_ chomp; # chomp defaults to $_ say "$. : $_"; # $. is the current line number } close $fh; # 1 : apple # 2 : banana # 3 : cherry ``` The gotcha: Tcl's `gets` strips the trailing newline for you; Perl's `<$fh>` keeps it, so call `chomp` when you do not want it. With no argument, `chomp` and `say`/`print` both default to `$_`, which is why the second loop never names the line variable. ## Fields and splitting Tcl's `split $line $sep` returns a list; Perl's `split /sep/, $line` returns an array, and `join` puts it back together. The difference: a Perl split separator is a *regex*, so you get the full pattern language for free. ```tcl set line "a:b:c" set parts [split $line ":"] puts [llength $parts] puts [lindex $parts 1] puts [join $parts ","] ``` ```perl use v5.42; my $line = "a:b:c"; my @parts = split /:/, $line; # split $line ":" (separator is a regex) say scalar @parts; # 3 (llength $parts) say $parts[1]; # b (lindex $parts 1) say join ",", @parts; # a,b,c (join $parts ",") ``` The gotcha: `split ' '` with a literal single-space string is a special case that splits on *any run of whitespace* and discards leading empty fields - the awk-style "split into words" behaviour, and usually what you want for free-form input. `split / /` (a regex with one space) is the literal "split on each single space" and does not collapse runs. Reach for `split ' '` to tokenise words: ```perl use v5.42; my @words = split ' ', " the quick brown "; say "@words"; # the quick brown ``` ## I/O, pipes, and in-place edit Tcl's `open`/`gets`/`puts`/`read`/`close` map to Perl's `open`/`<$fh>`/ `print`/slurp/`close`. The idiom is the three-argument `open` with a lexical filehandle. Tcl's `exec` - run a command and capture its output - becomes Perl's `qx{...}` (backticks) or `system` when you do not need the output. ```tcl set out [exec echo hello from shell] puts "exec said: $out" ``` ```perl use v5.42; my $out = qx{echo hello from shell}; # exec -> qx// (backticks) chomp $out; say "exec said: $out"; # exec said: hello from shell ``` Tcl has no in-place file edit built in; you read, transform, and write back. Perl's `-i` switch is the one-liner form of exactly that, and the runnable full-script equivalent is a read-modify-write you can write today: ```perl use v5.42; my $path = '/tmp/tcl_inplace.txt'; # holds: red green blue open my $in, '<', $path or die "read: $!"; my @lines = <$in>; # slurp all lines (list context) close $in; s/e/E/g for @lines; # transform each line's $_ open my $out, '>', $path or die "write: $!"; print {$out} @lines; # write them back close $out; # file now holds: # rEd # grEEn # bluE ``` The gotcha: `open` returns false on failure rather than raising (Tcl throws an error you catch with `catch`), so the `or die "...: $!"` idiom is mandatory - `$!` is the system error string. For pipes, Perl opens a pipe filehandle directly: `open my $fh, '-|', 'ls', '-l'` reads a command's output and `open my $fh, '|-', 'sort'` writes to a command's input, the structured form of Tcl's `open "|command"`. ## One-liners and switches Tcl is not a one-liner language, so this section is brief - but Perl is, and once you leave Tcl behind it is worth knowing the reflex exists. The switch family: `-e` supplies the program, `-n` wraps it in a no-auto-print loop, `-p` wraps it in an auto-print loop, `-i` edits in place, and `-a`/`-F` autosplit each line into `@F`. `BEGIN { }` and `END { }` blocks run before and after the loop. ```perl perl -ne 'print if /error/' log.txt # print matching lines perl -pe 's/\d+/N/g' input.txt # mask numbers perl -nE 'END { say $n } $n++ if /error/' log.txt # count, END block ``` These are shown without output because the implicit-loop switches are not recognised by this `pperl` build yet (see the note at the top). Each maps to a verified explicit-loop form: `perl -ne 'CODE'` is `while (<$fh>) { CODE }`, and `perl -pe 'CODE'` is the same with a trailing `print`. The runnable "count matching lines" form: ```perl use v5.42; open my $fh, '<', '/tmp/tcl_data.txt' or die "open: $!"; my $n = 0; while (<$fh>) { $n++ if /banana/; # the body of -ne } say $n; # 1 ``` The gotcha: until the switches land on `pperl`, write the explicit loop (it runs today and is what the switches expand to anyway), and consult the [One-Liners guide](../oneliner/index) for the full switch catalogue and recipes. Perl also has full subroutines, references, modules, and OO if your script outgrows a glue task: Tcl's `namespace eval` and `[incr Tcl]` classes map to Perl packages and the modern `class` syntax; see the [Object-oriented Programming guide](../oop/index) for that side of the language. ## Gotchas coming from Tcl The traps, collected for a final scan before you write: - **Two comparison families.** `==`/`<`/`<=>` compare numerically; `eq`/`lt`/`cmp` compare as strings. `"10" == "10.0"` is true, `"10" eq "10.0"` is false. Pick by the operation, not the value - the one quiet bug the string-first worldview invites. - **The sigil follows the element.** One element of `@items` is `$items[0]` (a scalar), not `@items[0]`; one value of `%a` is `$a{red}`. The container sigil and the element sigil differ. - **`sort` is string-sorted by default,** like comparing with `eq`. Pass `{ $a <=> $b }` for numeric order. - **`s///` returns a count, not the new string,** and edits in place. Copy first (`(my $t = $s) =~ s/.../.../`) or use the `/r` flag for a fresh string. - **`tr///` (the `string map` analogue) is a literal character set, not a regex.** `tr/ab/AB/` maps single characters. For patterns, use `s///`. - **`split ' '` and `split / /` differ.** The literal-space string splits on whitespace runs and drops leading empties (the word tokeniser); the one-space regex does not. Use `split ' '` for free-form input. - **`<$fh>` keeps the trailing newline,** unlike Tcl's `gets`. Call `chomp` to strip it. - **`open` does not raise on failure.** Use `open my $fh, '<', $path or die "...: $!"`. `$!` is the system error string; there is no `catch` to wrap it. - **References replace `upvar`.** To mutate a caller's variable, pass a reference (`\$x`, `\@arr`, `\%h`) deliberately rather than aliasing by name. References are also how Perl nests data. - **A `{ ... }` block is code, not a deferred string.** Perl parses blocks at compile time; there is no "the body is a string until I run it" step. Only `''`/`""` carry Tcl's `{}`/`""` substitution distinction. - **The implicit-loop switches (`-n`/`-p`/`-a`/`-F`/`-i`/`-l`) are not yet recognised by this `pperl` build.** Only `-e` works today. Write the explicit `while (<$fh>) { ... }` loop, which is exactly what the switches expand to, until they land.