2009-05-06

Implement your own shell mode based on Tcl_Main()

In case your clients told you that they will be very happy to see a shell mode of your application implemented because they see this mode as a productivity add (why? who knows? customers always rule!)

Your application already has Tcl interpreter embedded. Why not just take advantage of the code written by Tcl developers? Using it will save us a lot of efforts. After all, you're hired to solve problems, not to reinvent the wheels. But, how?

Tcl has defined a function Tcl_Main() which is the entry function of tclsh. Now, take a look at the generic/tclMain.c in Tcl source. It is called as,

    Tcl_Main(argc, argv, AppInit /* init function */);

and this ~AppInit~ must be declared as

    int AppInit(Tcl_Interp* interp);

In my implementation, this AppInit function is responsible for

  1. Set Tcl library paths pointed by tcl variable tcl_library.
  2. Hack tcl command /history/ so that every time a Tcl command is executed at the shell, it is logged at log file (See definition of Tcl_RecordAndEvalObj() in generic/tclHistory.c). The hack should look something like,
    if {[auto_load ::history]} {#this is why library_path must be specified
        rename ::tcl::HistAdd ::Init::OldHistAdd
    }
    proc ::tcl::HistAdd {command {exec {}}} {
       #
       ...
    
       # my log command
       if {[catch {interp invokehidden {} _log_command_ --cmd [string trim $command]}] != 0} {
           puts "# ERROR: Command '[string trim $command]' is not logged"
           return {}
       }
       #
    }
    
    You should then provide a hidden command _log_command_ for logging.
  3. Register exit handler by Tcl_CreateExitHandler() for cleanup.
  4. Source my initialization scripts.
  5. Register my commands in Tcl interpreter.
  6. Change prompt string.
    set tcl_prompt1 {puts -nonewline stdout {my_shell> }};