For AI agents: This is a paced, contrast-driven tutorial that teaches Eagle to someone who already knows Tcl. It assumes the reader has completed the official Tcl Tutorial and covers only where Eagle differs from Tcl, plus the features Eagle adds. For reference material, see core_language.md, tips_and_tricks.md, and the per-command deep-dives. To embed Eagle in a C# host, see embedding.md.
Eagle (Extensible Adaptable Generalized Logic Engine) is a Tcl-compatible
scripting language implemented in managed .NET code. If you know Tcl, you already
know most of Eagle — so this tutorial does not re-teach set, if, proc, or
list. Instead it walks the same ground the Tcl tutorial covers and stops only
where Eagle behaves differently, then goes on to the capabilities Eagle adds on
top (deep .NET interop, sandboxing, plugins, and more).
Please work through the official Tcl Tutorial first. This document is written as a delta on top of it: each lesson in the first half links back to the matching Tcl tutorial lesson and then describes only what changes in Eagle. Eagle targets the Tcl 8.4 language baseline (plus selected 8.5/8.6 features), so the Tcl tutorial's fundamentals apply directly.
- Contrast first. Each lesson in Arc 1 opens with a Prereq deep-link to the Tcl tutorial lesson it parallels, then covers just the difference.
- Everything is verified. Every snippet was executed in an Eagle shell; where
Eagle and Tcl differ, both outputs are shown. Expected output appears as a
;# =>comment. - Follow the links for depth. Lessons point forward to the reference docs (e.g. object.md, safe.md, regexp.md) when you want the full story.
- Status: complete. All lessons are written — Arc 1 (the core-language deltas from Tcl) and Arc 2 (the Eagle-only capabilities), plus a capstone.
Start the interactive shell (a REPL) and type at the % prompt, or run a script:
EagleShell # interactive REPL
EagleShell -evaluate 'puts hi' # evaluate one script string
EagleShell -file script.eagle # run a script fileEverything below can be pasted into the REPL. (When embedding Eagle in a C# application instead, see embedding.md.)
Each lesson parallels one or more Tcl tutorial lessons and covers only the change. Every lesson below is written in full.
| Lesson | Parallels (Tcl Tutorial) | The Eagle delta |
|---|---|---|
| 1. Meet Eagle, and running it | Intro / Running / Output | It's .NET; Tcl 8.4 baseline; the shell, -evaluate, -file; version introspection |
| 2. Values, variables, True/False | Variables & substitution | Computed booleans render True/False, not 1/0 |
3. Numbers and [expr] |
Math 101 / Computers and Numbers | Base-10 decimal literals; integer overflow wraps (no bignum promotion); entier()/wide()/double(); Eagle-only operators (^^ -> <-> <<< >>> :=), extra functions (log2, sign, randstr, …), and the Unicode ∞/π/ℇ |
4. if and switch |
if / switch | True/False conditions; switch matching modes |
5. Loops (and do) |
while / for & incr | incr does not auto-create a missing variable; Eagle's do/while-until |
| 6. Procedures and scope | proc / args / scope | Named arguments (nproc/napply), apply, and the [scope] command |
| 7. Lists | lists–lsort | Largely identical; lindex/lset index-list gather; lmap, lget, lremove |
8. Strings and [format] |
string–format | Culture-aware comparison; 64-class string is; .NET String.Format; extended string map; format conversions use int width (%ld for wides) |
| 9. Regular expressions | regexp–102 | Backed by the .NET regex engine; -eval/-command regsub modes; -options |
| 10. Arrays and dicts | arrays / dicts | dict set doesn't auto-create; ordering/backends; array storage backends |
11. Files, exec, channels |
files / exec | exec does not error on a non-zero exit by default; .NET-backed file; channels |
12. info, source, packages & namespaces |
info / packages & namespaces | Extended info; no "creative reading/writing" — a namespace body's unqualified names are namespace-local (reach globals via ::/global), so Tcl's silent global fallback footgun is gone; and namespace support can be enabled/disabled at runtime (namespace enable), which Tcl cannot do |
13. Building commands: eval, subst, format |
eval–subst | No {*} argument expansion (an 8.5 feature outside the 8.4 baseline); use eval/concat idioms |
14. Errors, catch, and try |
errors / trace | catch/error/errorInfo as Tcl; try is finally-only (no Tcl-8.6 on/trap); .NET exceptions carry detail; the large Eagle-only [debug] command |
15. clock, args/env, leftovers |
args & env / clock | clock uses .NET format translation; free-form relative date parsing is not implemented |
| 16. Child interpreters | child interpreters | [interp] create/eval/alias; -safe children — the on-ramp to Arc 2 |
These have no counterpart in the Tcl tutorial — they are the reasons to reach for Eagle.
| Lesson | What it covers |
|---|---|
17. Talking to .NET: the [object] command |
Create/invoke .NET types, opaque handles, -alias, overload resolution with -parametertypes, disposal → object.md |
| 18. The two-language model | Dropping to CLR types; the [library] P/Invoke FFI; compiling C# from script; the [tcl] bridge to a real Tcl runtime |
| 19. Safety and sandboxing | Safe interpreters, hidden commands, policy callbacks, resource limits, AppDomain isolation (interp create -isolated), [debug secureeval] → safe.md, interp.md |
| 20. Packages and plugins | [package], [load]/[unload], plugin load-security options (-trustedonly/-publickeytoken/-isolated), and the Enterprise Edition plugins → load.md |
21. Events, after, threads, async |
The event loop, timers, background work, and Eagle's concurrency model |
| 22. Hosts and I/O | The [host] command and the IHost abstraction that routes console I/O → host.md |
| 23. Testing your scripts | The built-in [test] framework (test1/test2), constraints, and suites |
| Capstone | A small end-to-end script combining .NET interop with a sandboxed child interpreter |
Prereq: Tcl Tutorial — Introduction, Running Tcl, Simple Text Output.
Eagle is Tcl for the .NET Common Language Runtime. The language you type is Tcl; the engine underneath is managed code, and every value can cross into the .NET world when you want it to. Your first program is exactly what you'd expect:
puts "hello, world" ;# => hello, worldThe difference from Tcl is not in this script — it's in what is running it. Two practical consequences show up immediately:
Eagle announces itself as Eagle, and reports its Tcl-compatibility level separately from its own version.
puts $::tcl_platform(engine) ;# => Eagle (Tcl reports "Tcl")
puts [info tclversion] ;# => 8.4 (the language baseline)
puts [info patchlevel] ;# => 8.4.21 (Tcl-compat patch level)
puts [info engine PatchLevel] ;# => 1.0.9692.29848 (Eagle's own build number)info tclversion/info patchlevel describe the Tcl language level Eagle
implements (8.4-based); info engine describes the Eagle build. When you need
to branch on "am I running under Eagle?", test $::tcl_platform(engine) — the
script-library helper isEagle does exactly this.
That is the whole of Lesson 1: same language, different engine, and the engine is happy to tell you so. The rest of Arc 1 is about the handful of places where "same language" has an asterisk.
Prereq: Tcl Tutorial — Assigning values to variables and Evaluation & Substitutions 1–3.
Values, variables, word grouping, and the three substitutions ($, [ ], \)
work exactly as they do in Tcl. set and unset are unchanged:
set greeting "hello"
puts $greeting ;# => helloHere is the first real difference, and it is one you will notice constantly:
Any operation that computes a boolean renders it as
TrueorFalse, not1or0.
puts [expr {1 == 1}] ;# Eagle => True (Tcl => 1)
puts [expr {2 > 3}] ;# Eagle => False (Tcl => 0)
puts [string equal abc abc] ;# Eagle => True (Tcl => 1)
puts [expr {1 && 1}] ;# Eagle => True (Tcl => 1)
puts [string is integer 42] ;# Eagle => True (Tcl => 1)Comparisons, logical operators, string equal, string match, and the
string is classifiers all produce True/False. This is a rendering choice
for computed booleans — a boolean literal is preserved verbatim, just like a
numeric literal (more on that in Lesson 3):
puts [expr {true}] ;# => true (the literal is passed through unchanged)Does this break anything? In practice, almost never — because True/False
are still perfectly good boolean inputs, and Eagle still accepts 1/0/yes/
no/true/false everywhere a boolean is expected. Conditions just work:
if {3 < 5} {
puts "yes" ;# => yes
}Two habits keep you out of trouble:
-
Use a boolean result directly as a condition (
if {[string equal $a $b]} …) rather than comparing it to a literal. That said, Eagle is forgiving if you do compare —Trueequals1numerically and the string"True"as you'd expect:puts [expr {(5 > 3) == 1}] ;# => True puts [expr {(5 > 3) eq {True}}] ;# => True
-
Don't hand-parse output that "should be
1." If a script pipes a computed boolean somewhere that later does a literal== 1string match, prefer the boolean directly.
This is a specific instance of a broader Eagle trait you'll see throughout: it favors the explicit, human-readable form. Keep going — the next lesson has the other big surprise.
Prereq: Tcl Tutorial — Results of a command — Math 101 and Computers and Numbers.
[expr] is the same command, and simple arithmetic is unremarkable:
puts [expr {2 + 2}] ;# => 4But the numbers underneath are .NET numbers, and that produces two differences worth understanding well.
Tcl evaluates 0.1 + 0.2 with binary IEEE-754 doubles, so it famously yields
0.30000000000000004. Eagle evaluates decimal literals using .NET's
System.Decimal — a base-10 type with ~28–29 significant digits — so the result
is exactly what you wrote:
puts [expr {0.1 + 0.2}] ;# Eagle => 0.3 (Tcl => 0.30000000000000004)
puts [expr {0.1 + 0.2 == 0.3}] ;# Eagle => True (Tcl => 0)
puts [expr {1.0 / 3.0}] ;# Eagle => 0.3333333333333333333333333333When you genuinely want binary floating point (for speed, or to match another
system's IEEE behavior), ask for it explicitly with double():
puts [expr {double(0.1) + double(0.2)}] ;# => 0.30000000000000004Functions that are inherently irrational still return IEEE doubles — there is no exact base-10 value for them:
puts [expr {sqrt(2)}] ;# => 1.4142135623730951In Tcl 8.5+, an integer that overflows is silently promoted to an arbitrary-
precision "bignum." Eagle does not do this. An integer literal is a 32-bit
int; a value that needs more range is a 64-bit wide; and arithmetic that
exceeds the type wraps around, exactly as it would in C#:
puts [expr {2147483647 + 1}] ;# Eagle => -2147483648
;# (Tcl => 2147483648)
puts [expr {9223372036854775807 + 1}] ;# Eagle => -9223372036854775808
;# (Tcl => 9223372036854775808)This is deliberate: Eagle is explicit about width rather than silently changing a value's type behind your back. When you want a wider or arbitrary-precision result, you say so:
puts [expr {wide(2147483647) + 1}] ;# => 2147483648 (64-bit)
puts [expr {entier(9223372036854775807) + 1}] ;# => 9223372036854775808 (big integer)wide(x)forces the 64-bit path (use it before an operation that would otherwise overflow a 32-bitint).entier(x)promotes to an arbitrary-precision integer — this is the explicit equivalent of Tcl's automatic bignum promotion.int(x)truncates toward a 32-bitint;double(x)gives binary float.
puts [expr {int(3.9)}] ;# => 3
puts [expr {double(5)}] ;# => 5Like Tcl, Eagle keeps a numeric literal in the form you wrote it when no arithmetic forces a conversion — so a hex literal stays hex:
puts [expr {0x1F}] ;# => 0x1F (not 31)The theme of this lesson is the same as the last one: Eagle prefers to do exactly what you asked and make you name any change of kind (width or base) explicitly, rather than promote or reinterpret values silently. That single idea — explicit over auto-magic — explains most of Arc 1's remaining surprises.
Eagle's expression engine includes the Tcl 8.5/8.6 additions you may already know
(eq/ne, in/ni, **, isqrt, …), and then adds a family of operators with
no Tcl equivalent at all:
# Logical implication / equivalence / exclusive-or (on booleans):
puts [expr {1 => 0}] ;# => False (implication: !a || b)
puts [expr {1 <=> 1}] ;# => True (equivalence)
puts [expr {1 ^^ 0}] ;# => True (exclusive-or)
# Bitwise implication / equivalence:
puts [expr {12 -> 10}] ;# => -5 (~12 | 10)
puts [expr {12 <-> 10}] ;# => -7 (~(12 ^ 10))
# Bit ROTATION (vs << >> which shift and drop bits):
puts [expr {1 <<< 1}] ;# => 2 (rotate left)
puts [expr {16 >>> 1}] ;# => 8 (rotate right)
# String-ordering operators (Tcl has eq/ne; Eagle adds lt gt le ge):
puts [expr {"abc" lt "abd"}] ;# => True
# In-expression assignment (the target name is a quoted string):
puts [expr {"y" := 5}] ;# => 5 (and sets $y to 5)Because these extensions have no Tcl counterpart, precedence when you mix them can
surprise you — parenthesize freely. The full operator set and precedence are
in core_language.md, or run [info operators] to list them.
Alongside the standard functions (abs, sqrt, sin, pow, …), Eagle adds
several that Tcl has no equivalent for:
puts [expr {log2(8)}] ;# => 3 (log base 2)
puts [expr {logx(8, 2)}] ;# => 3 (log to an arbitrary base)
puts [expr {sign(-5)}] ;# => -1 (-1, 0, or 1)
puts [expr {truncate(3.7)}] ;# => 3 (toward zero)
puts [expr {pi()}] ;# => 3.141592653589793
puts [expr {e()}] ;# => 2.718281828459045
# Randomness — random() is crypto-strong:
puts [expr {random()}] ;# => a random 64-bit integer
puts [expr {randstr(16)}] ;# => a random 16-character stringEagle also implements the IEEE floating-point classification functions from Tcl
8.7 (TIP 521) — isnan, isinf, isfinite, isnormal, issubnormal,
isunordered — so even against the 8.4 baseline they're available to you now:
puts [expr {isnan(acos(2))}] ;# => True (acos(2) is Not-a-Number)
puts [expr {isinf(pow(10, 400))}] ;# => True (overflows to infinity)And a genuinely Eagle-only touch: expressions understand the Unicode symbols
∞ (infinity, U+221E), π (pi, U+03C0), and ℇ (Euler's number, U+2107)
directly, as first-class values — preserved verbatim when alone (like the literals
in §3.3), numeric in arithmetic:
puts [expr {∞ > 1000000}] ;# => True
puts [expr {-∞ < 0}] ;# => True
puts [expr {∞ + 1}] ;# => ∞ (infinity arithmetic)
puts [expr {isinf(∞)}] ;# => True
puts [expr {π * 2}] ;# => 6.283185307179586
puts [expr {π == pi()}] ;# => True (π is the pi() constant)
puts [expr {ℇ == e()}] ;# => True (ℇ is Euler's number, e())Other Eagle additions include round2/round3 (explicit rounding modes) and
typeof, decimal, datetime, and timespan for type conversion inside an
expression. Run [info functions] to list them all.
Prereq: Tcl Tutorial — Numeric Comparisons — if and Textual Comparison — switch.
if, elseif, else, and the fact that a condition is an [expr] are all
exactly as in Tcl. The only thing to carry over from Lesson 2 is that a
comparison evaluates to True/False — and if accepts those (and 1/0)
equally:
if {5 > 3} {
puts "bigger" ;# => bigger
}Like Tcl, if is an ordinary command that returns the value of the branch it
runs, so you can use it as an expression:
set label [if {5 > 3} {expr 10} else {expr 20}] ;# label => 10switch is likewise familiar — exact matching by default, plus -glob and
-regexp modes and a -- end-of-options marker — and it returns the value of
the matched body:
puts [switch 2 {
1 {expr 100}
2 {expr 200}
}] ;# => 200
switch -glob abc { a* { puts "starts with a" } } ;# => starts with a
switch -regexp abc { {^a.c$} { puts "matched" } } ;# => matched
puts [switch xyz {
abc {expr 1}
default {expr 99}
}] ;# => 99 (a default branch)Nothing here should surprise you — which is the point. Control flow is one of the areas where Eagle and Tcl are essentially identical.
Prereq: Tcl Tutorial — While loop and For and incr.
while, for, and foreach behave as in Tcl, including foreach's multi-
variable and parallel-list forms:
set sum 0
for {set i 1} {$i <= 3} {incr i} { incr sum $i }
puts $sum ;# => 6
foreach {a b} {1 2 3 4} { puts "$a-$b" } ;# => 1-2 then 3-4Here is the delta, and it is a deliberate one:
incrdoes not create the variable if it doesn't exist.
In Tcl, incr count on an undefined count treats it as 0 and creates it.
Eagle refuses — silently creating a variable is treated as a footgun (a typo'd
name would spring into existence):
unset -nocomplain count
incr count ;# Eagle: error -> can't read "count": no such variable
;# Tcl: silently creates count = 1The fix is one line, and it makes the intent explicit:
set count 0
incr count ;# => 1
incr count 5 ;# => 6You'll meet this same "no silent creation" rule again with [dict set] and
inside namespaces (Lesson 12). It is Lesson 3's "no silent promotion" applied to
variables: Eagle does what you said, not what it guessed you meant.
A small addition: Eagle has a do loop, for when the body must run at least
once — in both while (repeat while true) and until (repeat until true) forms:
set n 0
do { incr n } while {$n < 3} ;# n => 3
set n 0
do { incr n } until {$n >= 3} ;# n => 3Prereq: Tcl Tutorial — proc, proc arguments, and Variable scope.
proc, default arguments, the args catch-all, global, and upvar are all as
in Tcl, and Eagle also supports apply (anonymous procedures):
proc square {x} { return [expr {$x * $x}] }
puts [square 6] ;# => 36
puts [apply {{x} {expr {$x * 2}}} 21] ;# => 42Eagle adds nproc/napply, which define a proc/lambda whose arguments are
passed by name, as name value pairs — in any order, with defaults filling
in the rest. (The pairs use bare names, not -name options.)
nproc connect {host {port 80} {timeout 30}} {
return "$host:$port (timeout $timeout)"
}
connect host example.com timeout 5 ;# => example.com:80 (timeout 5)
connect host localhost ;# => localhost:80 (timeout 30)napply is the anonymous form:
napply {{name} { return "hi $name" }} name bob ;# => hi bobSometimes a procedure should remember state between calls without resorting to
a global. Eagle's [scope] command provides a named, persistent variable
environment you can open inside a proc:
proc counter {name} {
scope create -open -clone -args $name
if {![info exists count]} { set count 0 }
incr count
}
counter myCounter ;# => 1
counter myCounter ;# => 2
counter myCounter ;# => 3
scope destroy myCounterThe variables live in the named scope rather than the call frame, so they survive
across invocations. See core_language.md for the full
[scope] model (cloning, locking, namespace integration).
Prereq: Tcl Tutorial — The list through lsearch, lsort, lrange.
The list commands you know — list, lappend, lindex, lrange, llength,
lsearch, lsort, lreplace, linsert, split, join — are all present and
behave as in Tcl, along with the 8.5-era lrepeat, lreverse, and lassign:
puts [lrepeat 3 x] ;# => x x x
puts [lreverse {1 2 3}] ;# => 3 2 1
lassign {1 2 3} p q ;# p => 1, q => 2 (returns the leftover: 3)Eagle adds a few functional/utility commands:
puts [lmap x {1 2 3} {expr {$x * 2}}] ;# => 2 4 6 (transform → new list)
set matrix {a {b c d}}
puts [lget matrix 1 2] ;# => d (deep index into a *variable*)
puts [lremove {a b c d} 1] ;# => a c d (remove the element at index 1)Note lget takes a variable name (like lindex on a variable), whereas
lindex takes a value.
One genuine difference to know. When lindex (or lset) is given a single
argument that is itself a list of indices, Eagle treats it as a gather —
projecting the elements at those indices — whereas Tcl treats it as a deep
index:
puts [lindex {a b c d e} {1 3}] ;# Eagle => b d (elements 1 and 3)
;# Tcl => (empty; it deep-indexes "b" at 3)The Tcl-style deep index is unchanged in the multi-argument form:
puts [lindex {a {b c d} e} 1 2] ;# => d (element 1, then its element 2)Prereq: Tcl Tutorial — String Subcommands, String comparisons, and Modifying Strings — format.
The string command's core subcommands (length, index, range, compare,
match, first, last, tolower, toupper, trim, map, and the 8.5-era
reverse/repeat/totitle) all work as expected:
puts [string reverse abc] ;# => cba
puts [string repeat ab 3] ;# => ababab
puts [string totitle hello] ;# => Hello
puts [string map {a A o O} foobar] ;# => fOObArTwo Eagle characteristics are worth flagging:
-
string isis much richer — Eagle ships ~64 classifier classes. Beyond the standard Tcl ones (integer,double,boolean,alpha, …), it adds many practical validators, all renderingTrue/False(Lesson 2):puts [string is double 1.5] ;# => True puts [string is inetaddr 192.168.1.1] ;# => True puts [string is inetaddr not.an.ip] ;# => False puts [string is uri https://example.com] ;# => True puts [string is path /usr/bin] ;# => True puts [string is guid 6b4f29fc-c7e9-4dbc-b838-909ae6708f5d] ;# => True puts [string is version 1.2.3] ;# => True puts [string is list {a b c}] ;# => True puts [string is dict {a 1 b 2}] ;# => True puts [string is base64 aGVsbG8=] ;# => True
Others include
cidr,xml, andelement— a quick way to validate input without hand-rolling a regex. -
Comparisons are culture-aware —
string compare/equal/matchrun on .NET's culture and comparison-option machinery (see string.md).
[format] is the C-printf-style command you know, but a bare integer
conversion (%d, %x, %o, …) uses Eagle's 32-bit int — the same
width-specifier semantics C and Tcl have on a 32-bit-int platform. Use the l
length modifier (%ld, %lx) for 64-bit wides:
puts [format %d 4294967296] ;# => 0 (2^32, truncated to 32 bits)
puts [format %ld 4294967296] ;# => 4294967296 (wide)
puts [format %x -1] ;# => ffffffff (32-bit)
puts [format %lx -1] ;# => ffffffffffffffff (64-bit)This is the string-side echo of Lesson 3: widths are explicit. If you may be
formatting values beyond 32 bits, reach for %ld/%lx. The extended string map (-nocase, -regexp, -eval, …) and the full format/scan behavior are
covered in string.md.
Prereq: Tcl Tutorial — Regular Expressions 101 through Regular Expressions 102.
regexp and regsub work the way you expect, and the patterns you know port
over — but the engine underneath is .NET's System.Text.RegularExpressions,
not Tcl's ARE engine. Eagle configures it to behave like Tcl where it matters —
most visibly, . matches a newline by default (Tcl's behavior, and the opposite
of raw .NET):
regexp {(\d+)} "abc123" m n
puts "$m / $n" ;# => 123 / 123
puts [regexp -all -inline {\d+} "a1b22c333"] ;# => 1 22 333
puts [regexp {a.b} "a\nb"] ;# => 1 (dot matches newline, as in Tcl)
puts [regsub -all {\d} "a1b2" X] ;# => aXbXThe differences are additive — regsub gains dynamic replacement modes on
top of literal substitution. The cleanest is -command (Tcl TIP #463): the
replacement is a command prefix, the matched text is appended, and the command's
result becomes the replacement:
proc doubleIt {m} { expr {$m * 2} }
puts [regsub -all -command {\d+} "a1b22c333" doubleIt] ;# => a2b44c666
puts [regsub -all -command {\d+} "a1b22c333" {string length}] ;# => a1b2c3There is also -eval (run a script per match) and -options (pass .NET
RegexOptions directly), plus -compiled, -line, and more. Because these
interact with the .NET engine — and a few advanced constructs (named captures,
some class shorthands) follow .NET syntax — see regexp.md for the
full treatment and the Tcl→.NET substitution translation.
Prereq: Tcl Tutorial — Associative Arrays, More On Arrays, and Dictionaries.
Arrays and dict behave as in Tcl — dictionaries include the 8.5 command and,
like Tcl, iterate in insertion order:
array set a {x 1 y 2}
puts "[lsort [array names a]] ([array size a])" ;# => x y (2)
puts [dict get {a 1 b 2} a] ;# => 1
puts [dict keys [dict create c 3 a 1 b 2]] ;# => c a b (insertion order)The one delta is the rule you met in Lesson 5: dict set won't create the
variable for you. In Tcl, dict set d k v creates d when it's undefined;
Eagle requires it to exist first:
unset -nocomplain d
dict set d k v ;# Eagle: error -> variable not found in call frame
;# Tcl: silently creates d = {k v}
set d [dict create] ;# create it explicitly, then...
dict set d k v
puts [dict get $d k] ;# => vEagle also offers extra array storage backends (environment variables, databases,
and more) behind the familiar array interface — see array.md.
Prereq: Tcl Tutorial — File Access, Invoking Subprocesses, and Channel I/O.
File and channel operations — open, close, gets, read, puts, file,
glob — behave as in Tcl (backed by .NET I/O underneath):
puts [file join a b c] ;# => a/b/c
puts [file tail /x/y/z.txt] ;# => z.txt
puts [file extension archive.tar.gz] ;# => .gzOpening, writing, and reading channels is likewise unchanged:
set f [open demo.txt w]
puts $f "line 1"
puts $f "line 2"
close $f
set f [open demo.txt r]
set first [gets $f] ;# read one line => line 1
set rest [read $f] ;# read the remainder
close $f
file delete demo.txtThe behavior to internalize is exec:
By default,
execdoes not raise an error when the child process exits with a non-zero status.
puts [exec echo hello] ;# => hello
set rc [catch {exec sh -c {exit 3}} output]
puts $rc ;# Eagle => 0 (no error)
;# Tcl => 1 (exec raises an error)In Tcl, a non-zero exit is an error you must catch; Eagle treats the exit code
as data rather than an exception by default, and you inspect it explicitly when
you care. See exec.md for retrieving the exit code and for Eagle's
argument-quoting rules (which also differ from Tcl's).
Prereq: Tcl Tutorial — info through packages & namespaces.
info, source, and package work as in Tcl — and Eagle's info adds many
subcommands (info engine, richer info commands filters, and more). Note that
info exists returns 1/0, not True/False: the boolean rendering
from Lesson 2 applies to computed booleans, while info exists keeps Tcl's
predicate convention.
set v 1
puts "[info exists v] / [info exists nope]" ;# => 1 / 0The lesson's real content is namespaces, where Eagle deliberately removes one of Tcl's classic footguns — the "dangers of creative writing".
In Tcl, code inside namespace eval that uses an unqualified variable which does
not exist in that namespace silently falls back to a like-named global —
reading it ("creative reading") or writing/clobbering it ("creative writing").
Eagle does neither: inside a namespace an unqualified name refers to a namespace
variable only (just as a proc's unqualified names are its locals). To reach a
global, use :: or the global command.
set x 1
# Creative READING — Tcl returns the global's value; Eagle refuses:
namespace eval ns { set x }
;# Eagle => error: can't read "x": no such variable
;# Tcl => 1
# Creative WRITING — Tcl clobbers the global; Eagle makes a namespace variable:
namespace eval ns2 { set x 99 }
puts "global=$x ns2=[set ns2::x]"
;# Eagle => global=1 ns2=99 (the global is untouched)
;# Tcl => the global becomes 99, and ns2::x was never createdDeclaring namespace variables works the usual way:
namespace eval foo { variable y 5 }
puts [set foo::y] ;# => 5This is Lessons 3 and 5's principle — no silent, surprising action — applied to name resolution. See namespace.md for the full model.
Prereq: Tcl Tutorial — Creating Commands — eval through subst.
eval and subst are as in Tcl:
puts [subst {1 + 1 = [expr {1 + 1}]}] ;# => 1 + 1 = 2The difference is a missing feature, because Eagle tracks the Tcl 8.4
baseline: there is no {*} argument expansion (that arrived in Tcl 8.5).
Attempting it is a parse error, not silent misbehavior:
set args {a b c}
list {*}$args ;# Eagle => error: extra characters after close-brace
;# Tcl => a b cUse the pre-8.5 idiom instead — eval with the list spliced into the command:
proc greet {a b c} { return "$a-$b-$c" }
puts [eval greet $args] ;# => a-b-cWhen the elements may contain spaces or special characters, build the command
safely with list/linsert so each element stays a single word, then eval it:
puts [eval [linsert $args 0 greet]] ;# => a-b-cPrereq: Tcl Tutorial — Debugging & Errors and More Debugging — trace.
catch, error, return -code, and the errorInfo/errorCode variables all
work as in Tcl. catch returns the completion code and captures the result (and,
optionally, an options dictionary):
set code [catch {error "boom"} result]
puts "$code: $result" ;# => 1: boom
proc failing {} { return -code error "custom fail" }
puts [catch {failing} r] ;# => 1 (r is "custom fail")The options dictionary carries the full error detail, including Eagle's
-errorline:
catch {error "oops" "stack info" "MY CODE"} result options
puts $options
;# => -code 1 -level 0 -errorcode {MY CODE} -errorinfo {stack info} -errorline 0Two differences are worth knowing.
1. try supports finally, but not on/trap handlers. Eagle's try is
try body ?finally script? — the cleanup form. It does not have Tcl 8.6's
on <code> {...} / trap <pattern> {...} handler clauses. Use catch to
handle an error and try/finally to clean up:
try {
error "inner"
} finally {
puts "cleanup always runs" ;# runs, then the error propagates
}
# To HANDLE an error, use catch:
if {[catch { riskyThing } err]} {
puts "handled: $err"
}(Writing try {...} on error {...} {...} raises
wrong # args: should be "try script ?finally script?".)
2. Errors from the .NET layer carry .NET detail. Where Tcl reports a terse
divide by zero, Eagle surfaces the underlying exception:
catch {expr {1 / 0}} msg
puts $msg
;# Eagle => caught math exception: System.DivideByZeroException: Attempted to
;# divide by zero. ... (includes the .NET exception type and trace)
;# Tcl => divide by zeroFinally, Eagle ships a large, first-class [debug] command — an interactive
debugger with breakpoints, variable watchpoints, single-stepping, sandboxed
evaluation, and script bundling. It has no Tcl equivalent; see
debug.md.
Prereq: Tcl Tutorial — Command line arguments & environment, Leftovers — time, unset, and Time and Date — clock.
$argv/$argc, the env array, unset, and time all behave as in Tcl:
puts [info exists env(PATH)] ;# => 1 (the environment is in env(...))
puts [time {expr {1 + 1}} 1000] ;# => e.g. "0.03 microseconds per iteration"clock formats and scans dates, and Tcl's % format specifiers work — Eagle
translates them onto the underlying .NET date/time engine:
clock format 0 -format "%Y-%m-%d %H:%M:%S" -gmt true ;# => 1970-01-01 00:00:00
set t [clock scan "2020-06-15" -gmt true]
clock format $t -format "%Y-%m-%d" -gmt true ;# => 2020-06-15The one delta: clock scan handles absolute date/time strings, but not the
free-form relative expressions Tcl's parser accepts (now, tomorrow,
+1 day, next monday). Those raise a clean error rather than guessing:
clock scan "now" ;# Eagle => error: unable to convert date-time string "now"
;# Tcl => (the current time, as an integer)Compute relative times from clock seconds instead — "tomorrow" is just now plus
a day (86400 seconds):
puts [clock format [expr {[clock seconds] + 86400}] -format "%Y-%m-%d"]
;# => tomorrow's date, e.g. 2026-07-17Eagle also adds high-resolution timing (clock milliseconds/microseconds,
clock start/stop) and custom epochs; see clock.md.
Prereq: Tcl Tutorial — Child interpreters.
interp creates and drives nested interpreters, exactly as in Tcl:
interp create child
interp eval child {expr {2 + 2}} ;# => 4
puts [interp exists child] ;# => True
interp delete child
puts [interp exists child] ;# => FalseThe reason this lesson is the bridge into the second half of the tutorial is
-safe. A safe child interpreter has every dangerous command removed, so
untrusted code can run inside it without reaching the filesystem, processes, the
network, or .NET:
interp create -safe sc
puts [interp issafe sc] ;# => True
interp eval sc { exec echo hi }
;# => permission denied: safe interpreter cannot use command "exec"You grant a safe child exactly the capabilities you choose by installing an alias — a command in the child that runs a trusted procedure in the parent:
proc parentLog {msg} { return "logged: $msg" }
interp alias sc log {} parentLog ;# create "log" in sc -> parentLog here
interp eval sc {log "hello"} ;# => logged: helloThat is the core idea behind Eagle's sandboxing — and there is much more: command hiding, policy callbacks, resource limits, execution timeouts, and per-tenant isolation. That is exactly where Arc 2 begins; see safe.md and interp.md.
Arc 2 has no counterpart in the Tcl tutorial — these lessons are the capabilities
that make Eagle Eagle. We start with the big one: [object], which lets a
script create and drive arbitrary .NET objects.
An object is referenced by an opaque handle. Create one with -alias and the
handle becomes a command you call directly:
set sb [object create -alias System.Text.StringBuilder]
$sb Append "Hello"
$sb Append ", world"
puts [$sb ToString] ;# => Hello, worldStatic members are invoked on the type name; instance members on the handle:
puts [object invoke System.Math Sqrt 144.0] ;# => 12
set list [object create -alias System.Collections.ArrayList]
$list Add one ; $list Add two
puts "[$list Count]: [$list Item 0]" ;# => 2: oneWhen a method is overloaded, pin the one you mean with -parametertypes — which
works on the alias form too (the option is merged into the call):
$sb -parametertypes {System.Char} Append 65 ;# appends the char 'A' (65)Handles are disposed automatically at cleanup, or explicitly with
object dispose $handle (-nodispose at creation opts out). [object] has
40-plus sub-commands (create, invoke, load, members, foreach,
dispose, …) and is unsafe by default (unavailable in safe interpreters).
Full reference: object.md; the idiom of choosing the simplest
unambiguous call form is in tips_and_tricks.md.
Eagle's design thesis is that a loosely-typed scripting layer and a
strongly-typed .NET layer are better together than either alone: script the
flow, drop to typed .NET where you need types, speed, or an existing library.
Lesson 17 showed the main bridge — [object] — which turns any CLR type into
something a script can use. When the script layer can't (or shouldn't) do a job,
reach for the typed .NET type that can. For instance, clock won't parse relative
dates (Lesson 15), but .NET's DateTime does typed date arithmetic directly:
set dt [object create -alias System.DateTime 2020 6 15]
puts [$dt AddDays 1] ;# => 06/16/2020 00:00:00 (a .NET DateTime value)
puts [$dt DayOfWeek] ;# => Monday(A value-type result like a DateTime comes back as its string form; wrap it in
another [object] handle if you need to keep calling methods on it.)
Eagle offers three further ways to reach other languages and runtimes:
[library]— a P/Invoke-style foreign-function interface for calling native C functions directly, via dynamically generated delegates. See library.md.[tcl]— a bridge to a real Tcl interpreter: load an actual Tcl runtime and evaluate native Tcl, marshaling values both ways. Where Eagle deliberately omits a Tcl 8.5/8.6 feature, you can reach the genuine article. See tcl.md.- Compiling C# at runtime — Eagle can compile and run C# on the fly (through
the .NET compiler / CodeDOM, via the
compileCSharpfamily of library helpers), so a script can generate typed code when it needs to.
The through-line: stay in script for as long as it's the clearest tool, and step into a typed or native layer precisely when it earns its keep — minimal sufficient explicitness applied across languages.
Lesson 16 introduced the safe child interpreter; this is the fuller picture of how Eagle runs untrusted code with confidence.
A safe interpreter has every command lacking the "safe" attribute removed, so a script cannot reach the filesystem, processes, the network, the host, or .NET:
interp create -safe sc
interp eval sc { exec echo hi }
;# => permission denied: safe interpreter cannot use command "exec"Beneath that headline are several independent layers you control:
-
Command hiding — any command can be hidden or exposed per interpreter; hidden commands are unreachable from the child's scripts but still callable by the parent:
interp create c interp hide c pwd interp eval c { pwd } ;# => permission denied: ... hidden command "pwd" interp expose c pwd interp eval c { pwd } ;# works again
-
Capability grants via aliases — the parent installs exactly the trusted operations the child may call (Lesson 16).
-
Policy callbacks — decide, per invocation, whether a specific command is allowed.
-
Resource limits — cap recursion depth, script size, and other resources so a runaway script cannot exhaust the host.
-
Execution timeouts and cancellation — abort a long-running script from outside.
-
AppDomain isolation (
interp create -isolated) — on runtimes that have AppDomains (.NET Framework and Mono, not .NET Core / netstandard), the child runs in its own application domain: its own loaded assemblies and memory, a contained fault boundary, and full teardown when youinterp deleteit. Combine-isolatedwith-safefor defense in depth around genuinely untrusted or crash-prone code.
Together these make Eagle a practical multi-tenant sandbox. The full model —
including [debug secureeval] for trusted/signed evaluation — is in
safe.md and interp.md.
Eagle has Tcl's package system and adds a .NET plugin system on top.
Packages (script level) work as in Tcl — package provide, package require, package ifneeded:
package provide myPkg 1.2
puts [package require myPkg] ;# => 1.2
puts [expr {[lsearch -exact [package names] myPkg] >= 0}] ;# => TruePlugins are the .NET extension mechanism: a compiled assembly that adds
commands, functions, policies, and resources to an interpreter, loaded with
[load] and removed with [unload]:
# load ?options? fileName ?typeName?
# unload ?options? fileName ?typeName?Loading runs a security-verification pipeline, and options let you require a given assurance — or an isolation boundary — before a plugin is allowed to load:
load -trustedonly MyPlugin.dll ;# only if Authenticode-trusted
load -publickeytoken $hexToken MyPlugin.dll ;# only if the strong-name key matches
load -isolated MyPlugin.dll ;# into its own AppDomain-trustedonlyrequires the assembly to pass trust verification (an Authenticode signature from a trusted publisher); the strong-name counterpart is-verifiedonly.-maybetrustedonlyenforces the same in release builds but relaxes it in debug builds, and — unlike-trustedonly— is permitted inside safe interpreters.-publickeytoken <hex>pins the plugin to a specific strong-name signing key: the assembly's public-key token must equal the given value, so you load only assemblies signed by exactly the key you expect.-isolatedloads the plugin into a separate AppDomain — a fault and security boundary torn down cleanly on[unload]. Likeinterp create -isolated(Lesson 19), it requires an AppDomain-capable runtime (.NET Framework / Mono), not .NET Core / netstandard.
The Eagle Enterprise Edition ships several plugins on this mechanism (Harpy, Badge, Kapok, Zeus, and more). See load.md for the full loading/verification model and plugin lifecycle, and each plugin's own doc for its commands.
Eagle has Tcl's event loop and timers:
after 50 { set ::done yes }
vwait ::done ;# waits for the event; ::done => yes
set id [after 10000 { cleanup }]
after cancel $id ;# cancel a pending timerafter, vwait, and update behave as in Tcl. On top of that, Eagle exposes the
CLR's threading: an Interpreter is thread-safe and supports genuine
concurrent script evaluation — multiple threads can run scripts in one
interpreter at once, with only shared state (globals, the command/object tables)
serialized under a lock. Use per-thread interpreters for isolation; to cancel a
script from another thread, use the engine's thread-safe cancel entry point. The
concurrency model is covered from the embedding side in embedding.md.
Every bit of console-style I/O a script performs — what puts writes, what gets
reads, the prompt, the colors, the title — is routed through a host object,
which the [host] command queries and controls:
host title "My Application" ;# set the window/console title
host size ;# => e.g. 80 24 (columns rows)
host flags ;# the host's capability flags (Title, Color, ...)Because I/O flows through the host rather than straight to System.Console, an
embedding application can redirect it anywhere — a GUI text box, a log, a socket,
or nowhere at all (a headless service). That is what makes Eagle embeddable in
environments with no console. The [host] command and the IHost abstraction are
documented in host.md and interpreter_host.md;
the C# side — writing a custom host, with a worked WinForms example — is in
embedding.md.
Eagle ships a full test framework — the same one its own test suite uses — built
around the test command (with test1/test2 variants). A test names itself,
describes itself, runs a body, and compares the result:
test myFeature-1.0 {addition works} -body {
expr {2 + 2}
} -result 4Tests support -setup/-cleanup scripts, -constraints (skip a test unless a
named capability is present, e.g. windows or tcl86), -match modes
(exact/glob/regexp), and expected-error checking. They are normally run inside the
test harness, which selects tests and reports results; a lone test in a bare
shell reports SKIPPED until the harness selects it. See the Testing section of
tips_and_tricks.md.
One script that uses nearly everything: a trusted parent helper that reaches into .NET cryptography, exposed to a safe child interpreter through a single alias, so untrusted code can hash but cannot touch the system.
# A trusted helper (parent interpreter) that uses .NET cryptography.
proc sha256 {text} {
set sha [object create -alias System.Security.Cryptography.SHA256Managed]
set enc [object create -alias System.Text.UTF8Encoding]
set hash [$sha ComputeHash [$enc GetBytes $text]]
return [object invoke System.Convert ToBase64String $hash]
}
# A sandbox for untrusted code: a safe interpreter + exactly one granted capability.
interp create -safe sandbox
interp alias sandbox sha256 {} sha256
# The untrusted script can hash (via the alias) but cannot touch the system:
puts [interp eval sandbox {sha256 "hello"}]
;# => LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=
interp eval sandbox {catch {exec rm important.txt} err; puts $err}
;# => permission denied: safe interpreter cannot use command "exec"
interp delete sandboxThis one script draws on the whole tutorial: [object] to reach .NET (Lesson 17),
the two-language split (Lesson 18 — the crypto is a typed .NET library driven from
script), a safe child interpreter (Lessons 16 and 19), and an interp alias
that grants exactly one trusted capability into the sandbox. That is Eagle's value
in miniature — a scripting language that can reach the entire .NET platform, and
sandbox untrusted use of it.
You've now been through the whole language — Arc 1's deltas from Tcl and Arc 2's Eagle-only powers. That's the tutorial. For depth on any topic, the reference docs are the next stop:
- Language reference: core_language.md
- Eagle-specific idioms and best practices: tips_and_tricks.md
- .NET interop: object.md · Sandboxing: safe.md · Interpreters: interp.md · Plugins: load.md
- Embedding Eagle in a C# application: embedding.md