Ticol Tcl - Troubleshooting, Tips and Tricks


Troubleshooting

Here are a selection of issues which should be easy to resolve:

  • Ticol Tcl is ANSI-only and does not support Unicode characters. This is by design
  • The most likely fault is this. If a program exits loops or tests unexpectedly or loops 'hang' or where [if] statements have no effect or an unexpected effect, then you've probably used the wrong [option expression] setting. This can be configured either via an [option expression] command or via ticol.ini. Flow control commands such as [if] which take expressions which resolve to a boolean can be configured to expect either an expresson or a Tcl command. Be consistent in how this is configured to avoid bugs
    It's good practice to specify an option expression statement at the start of a program unless you permanently configure it one way or the other in ticol.ini
    # Examples of bad and good expression presentation
    option expression on		# Enable 'expression on'
    if {file exists test} {		# BAD. [file]  is a command, not an expression
    if {[file exists test]} {	# CORRECT. Command return is evaluated even with 'option expression on'
    
    option expression off		# Disable 'expression on'
    if {file exists test} {		# Okay. [file] is a command and [if] is now expecting a command
  • If there is no output or activity past a certain point in the script then check for a malformed string and malformed escape sequences with an accidental string overrun to the end of the file. Use the /echo command line option to echo the preprocessor-rendered source code
  • Tcl command syntax is 'prefix' not 'infix'. Thus [$a + 2] is wrong for the command [+].  The correct syntax is [+ $a 2]
    Infix syntax is valid for expressions via the [expr] or [calc] commands or flow control with [option expression on]
  • Tcl looks a little like C/C++ but the language structure is quite different. Don't treat Tcl as if it were "C"
  • You can force a command to be evaluated within an [if] statement with [option expression on] by enclosing the command in square brackets
    Note that while braces look similar to those in C/C++ they perform a different function in Tcl! (see above)
  • In Tcl, take care as dereferencing a variable may excute code in some cases in a similar manner to C/C++

    set a "puts {Hello}"
    {*}$a     # Will print Hello

    proc foo {} {puts hello}
    set a foo
    $a          # Will call the function and print hello
  • To be safe, avoid dereferencing a variable which may contain a string which will execute Tcl code when testing for non-empty with [if]
    Use:  if{is_empty var} {...  rather than if{$var}
  • Always wrap command arguments and proc definitions  in braces. Avoid the temptation to omit braces within say [if] ladders
    e.g. use:  [if {condition}] {code}   and not: [if condition] code
  • Note that braces serve a specific function in Tcl. They delay (block) evaluation of the braced contents. Thus variables may not be resolved
    Although braces look similar to C/C++ their function is quite different. Be aware that braces block evaluation until that braced block is specifically evaluated
  • If a script halts or exits for no apparent reason use [option echo on], make full use of [catch] and [try catch], use the debugger to single-step by adding /BP to ticol.exe command line or using [option breakpoint on] and, if necessary, a [halt] statement before the suspected error
  • If you find you cannot run Windows commands such as dir, check the option autoexec setting using [option]
    Enable using:   option autoexec on
  • Ticol uses the @ sign not # sign in [upvar] and [uplevel] to specify an absolute level. The hash character is reserved for use by the Macro Preprocessor
    This simplifies handling of comments and to a degree, makes more syntactic sense   running 'at' a given level
    uplevel @0 set a 1
    upvar @0 s
  • Strings are not required to be wrapped in double quote characters. However, quoted strings behave differently to 'raw' unquoted ones. Quoted strings will be processed by the macro preprocessor as strings.
  • Visible double quotes within a script must be paired or the macro preprocessor will reject the code. Code such as:  "string       :is not allowed
  • Nested or unpaired double-quotes are not allowed  but you can emulate them using the [chr 34]  command or embedding a variable e.g. $quote as well as using backslash escaped quotes. "Quoting hell" is something to be avoided in Tcl.  Note also that braces can also be used to deliniate strings (See: help quoting hell)
  • If you run into complexities using escape (\) characters, you can use [escape] and [unescape] commands to help simplify
  • Long comment open/close sequences embedded in strings can cause problems unless protected by double quotes

    puts [ftp $hostname ls /tools/* -u admin -p xxxxx]            # Wrong. /* is visible to the macro preprocessor and will be interpreted as a long comment
    puts "[ftp $hostname ls /tools/*.zip -u admin -p xxxxx]"   # Correct. The MPP will ignore /*
  • If you have problems in escaping within strings then use the recommended 'minimal escaping style'. 
    Quotes are not always  required in Tcl since (just about) everything is a string anyway
    Don't use double-quotes where braces could be used or unless you want to force character hiding from the MPP
    Don't use braces where no backslash substitutions are otherwise needed
    'Naked' (unquoted or unbraced) strings are perfectly fine in many circumstances
  • A variable name may contain a space but must be quoted if it contains one.
    A variable name may also be one or more spaces!
    To retrieve a variable name comprised only of spaces you must use braces as follows

    set
    " " hello
    puts ${ }
  • Note that Ticol does not implement full namespaces. Namespaces are emulated
  • Pathnames typically require escaped backslash characters such as "\\".   See [escape] and [unescape] use
  • Data type ranges are almost identical to those found in C/C++ (32-bit) except that integers are, by default, evaluated as 64 bit
    See: help data type ranges
  • When commenting-out code it is good practice to place a space after a # comment symbol in Ticol. This prevents the accidental interpretation of a Tcl command as a Macro Preprocessor keyword. The most common mistake being to comment-out [if]
    #if {...}                     This is an MPP keyword IF statement
    # if{...}                     This is safe (space separates # from if)
  • Enums are provided but these work slightly differently to those in C/C++. An enum with no assignment is zero by default
  • Ticol does not use '0' as an octal number prefix. I really don't like this piece of design and have implemented 0oNN as an octal prefix
    To me, this behaviour makes no sense at all, especially when processing imported data which may therefore need to be "sanitised" for leading zeroes
    Non decimal numbers should be explicitly tagged. Additionally, hex numbers are recognised by most functions via 0xNN prefix and binary as 0bNN
    
    0123		# 123 decimal
    0o123		# 83 decimal
    0x123		# 291 decimal
    0b1111011	# 123 decimal
  • Binary structs are provided in Ticol but these are intended only for use with the calldll routines. Their use is slightly convoluted in order to satisfy this requirement
  • Proper classes are not provided but some class-like or object-like behaviour can be emulated using structs if desired
  • Some Tcl 'long form' math commands such as [not] , [and], [xor] etc. are not implemented as words. Use symbols such as [!], [&]
  • If you are uncertain how the macro preprocessor is rendering the source code you can use the command-line /echo option to echo the resolved code
  • In Ticol Tcl a degree of code checking and translation is performed by the Macro Preprocessor (MPP). This was implemented because Ticol is a non-bytecode interpreter and preprocessing the source code improves performance. In particular, [calc] used as an alternate to [expr] will result in optimised and expanded pure Tcl commands.
    The MPP is "C"-like but does not yet support nested #if constructs
    See: help macro
  • The # character is used in Ticol Tcl by the Macro Preprocessor and is not defined as a Tcl command
  • Function (command) call tracing can be used to help debug program flow using the trace command
  • See also: help faq   at the Ticol command line or "Frequently Asked Questions" in the manual
  • See also: help troubleshooting   at the Ticol command line or "Troubleshooting" in the manual

Top


Tips and Tricks

A selection of random tips:

  • You can load a program into the CLI using:   load <scriptname>
    The loaded program can be *.tcl or *.tcx
    You can inspect the loaded/macro-processed script using:   dump
    Note that TCX programs can't be listed although introspection can view components
    You can run the loaded script using:  run
  • The [run] command will allow a script to be loaded and executed with command-line arguments in one step
    e.g.  run foo.tcl 1 2 3
  • Self-elevating scripts can be constructed by using [is_elevated] and [elevate]
  • You can run either TCL or TCX files directly from the command line by associating the filetypes with ticol.exe
    I find it useful to associate TCX files and use these as if they were EXE files
  • To debug, you will need a [halt] command or an error condition to activate.
    To run in breakpoint/debugging mode use ticol.exe <scriptname> /bp or, in the code, use [option breakpoint on]
  • Use file-level modularity to break up scripts and avoid large, monolithic script files
        Break big scripts into smaller files and call using [source scriptname]
        Be aware that each call to [source scriptname] will evoke the macro
        preprocessor (MPP) which has a finite time cost
        Arguments to external files are permissible via [source scriptname]
  • Command line help is available at the console using:   ticol.exe /?
  • Topic help is available from the Ticol command-line using:   help <topic>
    Topics with spaces may be wrapped in double quotes but it isn't essential
  • If you're not sure of the help topic use find, e.g.:  find string
  • Unlike ActiveState Tcl, result echoing is not enabled by default in Ticol
    Use:   option echo on   to enable console echo output
  • An enhancement over [puts] and [format] is [printf] which works similarly to the C/C++ printf()

    printf
    "%s %.3f" "Hello world" $pi
  • [printf] has field width specifiers which use '*' and take a variable argument to specify the field width
  • You can use ticol.exe as an expression evaluator for batch scripts
    Use the /EXPR command-line argument (See: help /expr)
    e.g.   ticol.exe /expr:22.0/7.0
  • Creating obfuscated TCX scripts is easy. Just use:   ticol.exe <scriptname> /C
    This will create a new file of the same name but with the TCX file extension
    TCX files can't be double-encoded and also can't be decoded back to the original script
    Ensure you keep a backup copy of any original script before converting to TCX for excecution-locking etc.
  • TCX scripts can also be password protected or locked to particular environments via MAC addres, host name etc.
  • TCX scripts can hide passwords which often need to be embedded in system batch scripts.
    These scripts can then be locked to a particular workstation/host to prevent script copies being used elsewhere
  • Although [expr] will attempt to apply BEDMAS rules, you are recommended to make full use of brackets in expressions
  • Iterating arrays is quicker using [array foreach] than [array get] with [foreach]
  • You can initialise using data from another file by using [source scriptname]
  • You can profile script performance and spot bottlenecks using the /G command-line option
    Use:   ticol.exe <scriptname> /G    or, preferably:   ticol.exe <scriptname> /G /NA to ignore autoexec.tcl
  • [is_mod] offers a simpler and quicker way to take actions depending on a mod result
  • [goto_block] must have any initialisers set within a dummy entry. Freeform initialises or raw commands outside a braced initialiser are not allowed
  • [foreach] requires the initial arguments to be braced:
    Use: foreach {x y} [array get env] { puts $x=$y }
    not: foreach x y [array get env] { puts $x=$y }
  • Strings with C-like escape sequences don't work from the CLI.
    CLI interaction with Windows requires C-escapes to be disabled
    You can enable escapes within strings if you set the following options:

    option autoexec off
    option escape on
  • The Macro Preprocessor (MPP) doesn't replace macro variables within quoted strings
    Either brace your strings or break the string at the point where the macro variable is used
  • A common Macro Preprocessor error is to comment out an if statement as #if. This is a reserved macro command.
    Leave a space after any # comment character. Use '#  if' rather than '#if'
  • Return from a child script to the parent using [return -code exit]
  • [day_of_week] works only with the Gregorian calendar, not Julian
  • A procedure name may be empty but to call this you must use [call] or [ ]
    
    proc {} {} {puts hi}	# Procedure with no name
    call {}
  • A procedure may also be named using one or more spaces. Call using [call] or square brackets
    
    proc { } {} {puts hi}	# Procedure with one space as name
    [" "]			# Call the proc
    call " "		# Call the proc
  • Ticol can do lambda-like operations
    For more information see:  help lambda
  • You can make a new array constant by appending -const to the array declaration
    array foo { ... initialisers ...} -const
  • [upvar] allows a shortcut where the local variable name is omitted. In this case the local name will be the same as the referenced name
    If name conflicts arise you can still override by specifying a local name. The local name can be different
  • PERL-style multiple/double-dereferencing is supported using the dollar sign [set [set [set a]]]   is the same as $$$a
    You may still use multiple [set var] if you wish however, for many uses the repeated use of [set] is pretty 'clunky', particularly when handling complex variable references passed to functions (e.g. structs or struct fields).  Deeply nested/complex variable references can become confusing even with $$
  • You can comment out blocks of code using several methods.

    Use # Standard Tcl single-line comments
    Use C/C++ style /* ... */ long comments
    Use if {0} { ... }
    Use #ifdef with a macro-variable which has not yet been set, e.g. #ifdef comment
    Use a [goto_block] with a [goto] 'skip' command

    The, standard comment, "C" long-comment and macro are the most efficient since the MPP will completely excise the unreferenced code from the source
  • Single-line # comments don't need a semi-colon prefix such as ->  ;# Comment since # is not implemented as a Tcl command
  • Nested #ifdef...#endif statements are currently not supported
  • Source code line numbers can be entered into a script using the #__LINE__ macro.
    Other values can be entered using:

    #__DATE__   Returns an unformatted ISO date YYYYMMDD
    #__FILE__   The current full script name/path when called using [run]
    #__HOST__   The computer host name of the system being run on
    #__TIME__   The current time in 24 hour clock format as HH:MM:SS
    #__USER__   The currently logged-in username (if any)
  • [assert] is  useful during development of a script
    It can be globally disabled using #define NDEBUG as with C/C++
    If you #undefine NDEBUG later in the script be aware that the MPP runs fully  before the script is run so this
    may not have the effect you intended
  • You can set argc and $argv() consts using the following method (add -nocomplain if you rerun the code in the IDE)
    
    unset argc -const -nocomplain
    unset argv -const -nocomplain
    set argc 1
    set argv(0) Hello
  • The Macro PreProcessor (MPP) supports a wide range of commands including #if, #exec, #echo and #exit
    The #if command takes a Tcl expression and will spawn a separate interpreter instance during MPP preload
    Note that macro commands are all fully executed before a Ticol lscript is run
    The macros themselves are stripped from any script before it is run

    #if eq $env(computername) TORNADO
    #echo Running on TORNADO
    #else
    #echo Not running on TORNADO
    #endif
  • You can configure the maximum recursion level via ticol.ini using

    ; TICOL.INI
    [config]
    RecursionMax=<value>


    Where <value> is an integer between 1 and 5000. The default is 5000 and a good value would be 1000
  • A variable can be passed to a proc by name (reference) and accessed as a global variable as follows
    Preceding the argument reference variable with an underscore helps code clarity

    proc foo { _q} {
       puts "passed variable $_q which has value ${::$_q}"
    }

    set i 23
    foo i
  • A global struct can be referenced from a passed name in a proc using [struct set ::${structvar}.field] as follows
    proc foo {x} {
        upvar $x      # Not strictly required; to reference a global; use the scope prefix ::
        puts [type $x]
        puts "In proc foo: Global s.a is: '[struct set ::${x}.a]'"
        struct set ${x}.a Goodbye
        puts "Exit proc foo: Global s.a is: '[struct set ::${x}.a]'"
    }
    struct s {a 10}
    foo s
  • You can check at runtime if the current active script was encrypted using [info encrypted] which will return a boolean
  • It is possible to write limited scripts entirely using the Macro PreProcessor (MPP) since these can also call Tcl commands
  • Ticol internal encryption routines (including TCX obfuscation) use weak symmetric encryption.
    If you need stronger encryption, link to an external module e.g. via ticol_calldll.dll
  • Braced variables are dereferenced recursively with the innermost braces resolved first. This allows complex multi-level dereferencing
    The opening brace must follow immediately after the dollar $ sign
    Spaces are within curly-braces are treated as part of the variable name and a variable name may be one or more spaces
    
    set a 1
    set b1 2
    set c2 3
    ${a}			# -> a -> 1
    ${b{$a}}		#-> a -> b1 -> 2
    ${c${b${a}}}		# -> a -> b1 -> c2 -> 3
    set " " "Space!"
    ${ }			# -> Space!
  • Cleanly reset background colour highlighting in a text without it wrapping onto the next line by suppressing CRLF before calling [textcolor]
    
    textcolor white darkmagenta
    puts " * Hello world * " -nonewline
    textcolor
    newline

See the manual FAQ  and Tips section for more tips and tricks

Top


Improving Performance

Ticol is a pure interpreted script language. It does not perform any byte-code compilation or dynamic optimisation, so performance will be slower than other Tcl variants such as ActiveState. However, since Ticol was designed for day-to-day administrative script work this should not present any serious problems If performance is an issue then it is recommended to use another such as ActiveState Tcl

The Ticol Macro PreProcessor (MPP) is able to statically optimise some  aspects of the script source code but the result will still be interpreted
The MPP strips out comments, so they are not interpreted as commands by Ticol

Loop-processing constructs will not be as efficient as a compiled language such as C#. however many of the more complex list-processing or string-splitting operations are quite efficient. Ticol offers a wide range of commands and some plugin libraries which are "closer to C++" and therefore fast and efficient

You should avoid single-character loop-processing within Tcl, particularly for loops which require  complex branched if statements.

Preferably call a dedicated, external DLL function and use Ticol as the 'glue' to hold together more efficient components. A simple plugin
interface library is offered for C++ programmers.

The following commands are offered to enhance performance:

  • array item. Ambiguous addressing of arrays with default return instead of error
  • calc. Static, self-optimising equivalent to [expr]
    Translates expressions into Tcl commands in Polish Notation (NPN)
  • calldll. Call fast external DLL routines
  • chr. Return a character by number with optional offset
  • cmp. Efficient compare function
  • comma. Efficient comma-formatting of numbers
  • concat. Efficient string concatenation
  • [.] dot head will return the first character of a string
  • eq, eqi Efficient comparison commands
  • eqn, nen Shorthand equivalence/disequivalence
  • funct. Call any [expr] function directly. e.g.  [funct tan]
  • ge, gei *i is the case insignificant version ...
  • gt, gti Greater than (gti:ignore case)
  • index. Rapid single char index into a string
  • is_mod. Modular arithmetic test
  • le, lei Less than or equal (ignore case)
  • loop. A fast loop iterator
  • lt, lti Less than (lei:ignore case)
  • makestr. Create a new string
  • ne, nei Not equal (nei:ignore case)
  • rset. Assign to a string and align
  • setat. String manipulation
  • store. Very rapid, buffered memory storage
  • strsplit. Split a string
  • strstr. Locate a substring
  • tohex. Convert an integer to hexadecimal

Move proc definition, list evaluation and creation outside of loops if possible. Pre-evaluate lists into variables. Make full use of the macro-PreProcessor to optimise constants during the preprocessing phase Use [concat] when concatenating large strings. Avoid chaining the set operator as the accumulated string passes through the interpreter

    POOR:

    set out $out$ch

    GOOD:

    concat out $ch

Use [cmp], this compares string segments in memory, instead of extracting and then comparing the resulting extract in an expression. It can
potentially avoid copying large blocks of data. 'time' tests show it to be around 30% faster especially within loops

    POOR:

    if { eq [mid $data $i 2] "Hi" } {

    GOOD:

    if { cmp data "Hi" $i 2 } {

When extracting small sections of a string use [setat] to directly slice the section of a string to a variable, thus avoiding the use of the set call. This sets a variable directly with index values located at a specified position and length. Timing tests show [setat] is around 50% faster than using [mids] together with [set]

    POOR:

    set ch [mids $data $i 1]

    GOOD:

    setat ch $data $i 1

Character replacement can be performed rapidly using [replacechar] rather than re-assigning the result of [string replace]

    POOR:

    set s [readfile ticol.man]
    set s [string replace $s "\t" " "]

    GOOD:

    set s [readfile ticol.man]
    replacechar s "\t" " "

To concatenate strings, particularly large ones or large numbers of small ones use [append] instead of repeated calls to chained [set] commands. The use of [set] chains creates multiple copies of strings whereas append appends directly to the original variable. Append is faster than [lappend] and [store] is about 5 times faster than [append] but less flexible to use

    POOR:

    set a "The quick"
    set a "$a brown fox"
    set a "$a jumped over"
    set a "$a the lazy dog"

    GOOD:

    set a "The quick"
    append a " brown fox"
    append a " jumped over"
    append a " the lazy dog"

    BEST:

    store "The quick"
    store " brown fox"
    store " jumped over"
    store " the lazy dog"
    set a [store get]

A key to interpreter performance is pre-calculating as much as possible outside of intensive loops and making good use of intrinsic functions instead of synthesising them from other commands

String comparison for string prefixes should use the [eqn] and [nen] commands which will safely test equality or inequality for a given length. This would be effective in tight loops when comparing large string prefixes than using a compound of [eq] and [string]

    POOR:


    set a "The quick brown fox"
    if { eq [string left $a 3] "The" } { do something ... }

    GOOD:

    set a "The quick brown fox"
    if { eqn $a The 3 } { do something ... }

Use [for]. For loops are often far more efficient than while {} loops. [loop] is even more efficient

For accurate performance timing, use the 'time' command or run Ticol in "graph" mode with the /G command line argument [let] may be used for multiple, simultaneous assignments

Use [array_foreach] to iterate arrays if you need simple behaviour

Because it is a generalised expression-handler with significant checking-overheads, [expr] may be far slower than decomposing an expression into dedicated component operations with Tcl Polish 'prefix' syntax. The Macro PreProcessor will optimise the [calc] command and expand into raw Tcl statements. If the MPP is disabled then [calc] will be interpreted equivalent to [expr]

Example:

    set src [expr ($x & $y) % 3]	# Expression
    set src [% [& $x $y] 3]		# Decomposed expression (~50% faster)
    set src [calc ($x & $y) % 3]	# Expression is optimised by the MPP

See:  http://wiki.tcl.tk/348

Move command calls out of loop arguments and bodies where possible to avoid repeated calls to slower evaluation commands

Example:

    option expression off
    for {set i 1} {< $i [linkedlist count $handle]} {++ i} { # Slower
    set c [linkedlist count $handle]	# Evaluate once
    for {set i 1} {< $i $c} {++ i} {	# Faster 

Top


Debugging (Using the Debugger)

To run in breakpoint/debugging mode use ticol.exe <scriptname> /debug

Or use ticol.exe <scriptname> /bp or, in the code, use [option breakpoint on]

When a [halt] or error condition arises the debugger menu will launch

Consider the following code...

cls
option expression off
option breakpoint on
watch add i
halt

set i 0
for {} {< $ 10} {++ i} {
	if {> $i 5} {
		puts "Break condition met at i==$i"
		break
	}
	halt
}
newline
puts "Exit for with i==$i"
puts "* Done *"

We can run this using:   ticol.exe <scriptname> or ticol.exe <scriptname> /NA to avoid running the autoexec.tcl script

On running the debugger will be enabled and will halt at the command after first [halt] instruction
Note that the [halt] instruction itself is not traced. Thus we halt at the first [set] command
Note also that the 'local' line number is show on the left. This is the line number of the snippet of code being executed

debug-1.png (1641 bytes)

Variable i is enabled for a [watch] statement and is curently out of scope

Pressing [R] to run will cause execution to resume and then halt again and break at the first command after the next [halt] statement ...

debug-2.png (2769 bytes)

Pressing [R] again will run another iteration of the [for] loop and now, variable i is in scope and traces the value 0 ...

debug-3.png (3045 bytes)

Pressing [R] repeatedly will resume and run each iteration of the [for] loop and, in this example, will trace the value of variable i ...

debug-4.png (3796 bytes)

Pressing [R] repeatedly will evetually result in the loop exiting when variable i is greater than 5

So far we have used [R] to run to the next [halt] statement. Alternatively, the debugger can single-step with either [T] to trace showing the stack or  [N] to simply step to and execute the next instruction. The image below shows two [T] trace steps ...

debug-trace.png (8244 bytes)

Pressing [N] for next, skips all debugging information other than variable watch (if this is enabled) ...

debug-next.png (3408 bytes)

[W] will toggle Watch off or on, unless overridden by the [watch] command. See help watch.

The script may programatically abandon debugging by either clearing any [halt] command or issuing an [option breakpoint off] command

Top


Back | Top

Last updated on 04 March 2021 - This page is designed for 1024 x760 and higher resolution displays