Skip to content

Latest commit

 

History

History
1311 lines (984 loc) · 53.8 KB

File metadata and controls

1311 lines (984 loc) · 53.8 KB

The Eagle Tutorial (for Tcl Programmers)

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).


Prerequisite: the Tcl Tutorial

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.

How this tutorial works

  • 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.

Running the examples

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 file

Everything below can be pasted into the REPL. (When embedding Eagle in a C# application instead, see embedding.md.)


Roadmap

Arc 1 — You already know most of this (the deltas)

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 listslsort Largely identical; lindex/lset index-list gather; lmap, lget, lremove
8. Strings and [format] stringformat Culture-aware comparison; 64-class string is; .NET String.Format; extended string map; format conversions use int width (%ld for wides)
9. Regular expressions regexp102 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 evalsubst 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

Arc 2 — The Eagle-only powers (no Tcl parallel)

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

Lesson 1 — Meet Eagle, and running it

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, world

The 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.


Lesson 2 — Values, variables, and the first surprise: True / False

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           ;# => hello

Here is the first real difference, and it is one you will notice constantly:

Any operation that computes a boolean renders it as True or False, not 1 or 0.

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 — True equals 1 numerically 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 == 1 string 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.


Lesson 3 — Numbers and [expr]: a base-10 (.NET) numeric model

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}]          ;# => 4

But the numbers underneath are .NET numbers, and that produces two differences worth understanding well.

3.1 Decimal literals are base-10 and exact

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.3333333333333333333333333333

When 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.30000000000000004

Functions that are inherently irrational still return IEEE doubles — there is no exact base-10 value for them:

puts [expr {sqrt(2)}]            ;# => 1.4142135623730951

3.2 Integers have a fixed width and wrap (no bignum promotion)

In 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-bit int).
  • 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-bit int; double(x) gives binary float.
puts [expr {int(3.9)}]       ;# => 3
puts [expr {double(5)}]      ;# => 5

3.3 Literals are preserved

Like 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.

3.4 Operators Tcl doesn't have

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.

3.5 Extra math functions

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 string

Eagle 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.


Lesson 4 — if and switch

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 => 10

switch 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.


Lesson 5 — Loops, and the incr footgun that isn't

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-4

Here is the delta, and it is a deliberate one:

incr does 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 = 1

The fix is one line, and it makes the intent explicit:

set count 0
incr count        ;# => 1
incr count 5      ;# => 6

You'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 => 3

Lesson 6 — Procedures, named arguments, and persistent scope

Prereq: 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]              ;# => 42

Named (keyword) arguments: nproc and napply

Eagle 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 bob

Persistent state: [scope]

Sometimes 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 myCounter

The 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).


Lesson 7 — Lists

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)

Lesson 8 — Strings and [format]

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]  ;# => fOObAr

Two Eagle characteristics are worth flagging:

  • string is is much richer — Eagle ships ~64 classifier classes. Beyond the standard Tcl ones (integer, double, boolean, alpha, …), it adds many practical validators, all rendering True/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, and element — a quick way to validate input without hand-rolling a regex.

  • Comparisons are culture-awarestring compare/equal/match run on .NET's culture and comparison-option machinery (see string.md).

[format] uses the platform integer width

[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.


Lesson 9 — Regular expressions

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]                  ;# => aXbX

The 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}]  ;# => a1b2c3

There 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.


Lesson 10 — Arrays and dictionaries

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]     ;# => v

Eagle also offers extra array storage backends (environment variables, databases, and more) behind the familiar array interface — see array.md.


Lesson 11 — Files, exec, and channels

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]   ;# => .gz

Opening, 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.txt

The behavior to internalize is exec:

By default, exec does 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).


Lesson 12 — info, source, packages, and namespaces

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 / 0

The 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 created

Declaring namespace variables works the usual way:

namespace eval foo { variable y 5 }
puts [set foo::y]                              ;# => 5

This is Lessons 3 and 5's principle — no silent, surprising action — applied to name resolution. See namespace.md for the full model.


Lesson 13 — Building commands: eval, subst, and no {*}

Prereq: Tcl Tutorial — Creating Commands — eval through subst.

eval and subst are as in Tcl:

puts [subst {1 + 1 = [expr {1 + 1}]}]          ;# => 1 + 1 = 2

The 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 c

Use 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-c

When 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-c

Lesson 14 — Errors, catch, and try

Prereq: 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 0

Two 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 zero

Finally, 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.


Lesson 15 — clock, arguments, and the environment

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-15

The 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-17

Eagle also adds high-resolution timing (clock milliseconds/microseconds, clock start/stop) and custom epochs; see clock.md.


Lesson 16 — Child interpreters (and a first look at safety)

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]               ;# => False

The 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: hello

That 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.


Lesson 17 — Talking to .NET: the [object] command

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, world

Static 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: one

When 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.


Lesson 18 — The two-language model

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 compileCSharp family 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 19 — Safety and sandboxing

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 you interp delete it. Combine -isolated with -safe for 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.


Lesson 20 — Packages and plugins

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}] ;# => True

Plugins 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
  • -trustedonly requires the assembly to pass trust verification (an Authenticode signature from a trusted publisher); the strong-name counterpart is -verifiedonly. -maybetrustedonly enforces 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.
  • -isolated loads the plugin into a separate AppDomain — a fault and security boundary torn down cleanly on [unload]. Like interp 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.


Lesson 21 — Events, after, and threads

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 timer

after, 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.


Lesson 22 — The host and I/O

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.


Lesson 23 — Testing your scripts

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 4

Tests 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.


Capstone — .NET interop inside a sandbox

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 sandbox

This 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.


What's next

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: