[slebetman]: Here's my first attempt at a pure-tcl implementation of readline-like capabilities. Save this into a file '''tclline.tcl''' and source it from an interactive [tclsh]. The good news is that console programs like [vi] and links and even the [Linux Console Text Editor In Pure TCL] works perfectly with this. If you are brave enough with it you can even source it from your '''.tclshrc''' file. [ak]: Note that the '`exec stty ...`' is unix specific. It is also the one part we cannot do in pure Tcl. It is either the [exec], or a C extension (like Expect (I think Expect can do the same changes to the pty stty is able of. IIRC)). ''([slebetman]: ak, I edited out your comment on colors since it no longer applies to the current code. Hope you don't mind. I'll edit out my comment here as well in a couple of days if nobody objects)'' [slebetman]: Quite true ;) I guess this really should be called '''Unix/Linux Readline in Pure Tcl'''. Actually, ''[stty]'' is not the only dependency. This also assumes a VT100/ANSI/xterm/linux terminal. The escape sequences won't work with things like the COMSPEC shell or even [Tkcon]. [slebetman] 10 Mar 2009: Fixed [glob] * substitution to not substitute in braces{}. This is so that the expand operator [{*}] and [regexp]s don't need to be backslash escaped. [slebetman] 14 Jul 2006: Implemented proper (not so buggy) word completion. Word completion first searches the filesystem, then falls back to tcl commands & procs and finally to (global?) variables. Implemented the '''alias''' and '''unalias''' commands. I needed them to support coloring in ''ls'' and ''vi'' (so I can do: ''alias ls {ls --color=auto}''). Also made history persistent by saving to a `.tclline_history` file and implemented a `.tcllinerc` file. The only remaining major features that's not yet implemented are long line editing and multi-line editing. [slebetman]: New version. This now properly supports long line editing. Word completion now colours hints: commands are green, variables magenta everything else not coloured. I've also implemented "universal" command substitution which substitutes not only Tcl commands and procs but also substitutes executables via [exec]. So now you can do things like: '''`set x [[split [[cat /etc/mtab]] \n]]`'''. Multi-line editing is still not implemented. [slebetman] 18 Jul 2006: Finally, multi-line editing is implemented! The main loop is also now a fileevent so this should use even less CPU time. Also implemented glob substitution of * and ~ (if you need literal '''*''' or '''~''' on the command line they can be escaped using backslashes: '''\*''' and '''\~'''. Also fixed a bug with alias handling in unknown. [slebetman] 20 Jul 2006: Tclline now uses [Tclx] if available to prevent SIGINT from causing the shell to exit. Also fixed a bug in command completion (basically I forgot to put -- in glob). With this modification, I'm now using tclsh as my login shell. ---- **Code** ====== #! /usr/bin/env tclsh # tclline: An attempt at a pure tcl readline. # Use Tclx if available: catch { package require Tclx # Prevent sigint from killing our shell: signal ignore SIGINT } # Initialise our own env variables: foreach {var val} { PROMPT ">" HISTORY "" HISTORY_BUFFER 100 COMPLETION_MATCH "" } { if {![info exists env($var)]} { set env($var) $val } } foreach {var val} { CMDLINE "" CMDLINE_CURSOR 0 CMDLINE_LINES 0 HISTORY_LEVEL -1 } { set env($var) $val } unset var val array set ALIASES {} set forever 0 # Resource & history files: set HISTFILE $env(HOME)/.tclline_history set RCFILE $env(HOME)/.tcllinerc proc ESC {} { return "\033" } proc shift {ls} { upvar 1 $ls LIST set ret [lindex $LIST 0] set LIST [lrange $LIST 1 end] return $ret } proc readbuf {txt} { upvar 1 $txt STRING set ret [string index $STRING 0] set STRING [string range $STRING 1 end] return $ret } proc goto {row {col 1}} { switch -- $row { "home" {set row 1} } print "[ESC]\[${row};${col}H" nowait } proc gotocol {col} { print "\r" nowait if {$col > 0} { print "[ESC]\[${col}C" nowait } } proc clear {} { print "[ESC]\[2J" nowait goto home } proc clearline {} { print "[ESC]\[2K\r" nowait } proc getColumns {} { set cols 0 if {![catch {exec stty -a} err]} { regexp {rows \d+; columns (\d+)} $err -> cols } return $cols } proc prompt {{txt ""}} { global env set prompt [subst $env(PROMPT)] set txt "$prompt$txt" foreach {end mid} $env(CMDLINE_LINES) break # Calculate how many extra lines we need to display. # Also calculate cursor position: set n -1 set totalLen 0 set cursorLen [expr {$env(CMDLINE_CURSOR)+[string length $prompt]}] set row 0 set col 0 # Render output line-by-line to $out then copy back to $txt: set found 0 set out [list] foreach line [split $txt "\n"] { set len [expr {[string length $line]+1}] incr totalLen $len if {$found == 0 && $totalLen >= $cursorLen} { set cursorLen [expr {$cursorLen - ($totalLen - $len)}] set col [expr {$cursorLen % $env(COLUMNS)}] set row [expr {$n + ($cursorLen / $env(COLUMNS)) + 1}] if {$cursorLen >= $len} { set col 0 incr row } set found 1 } incr n [expr {int(ceil(double($len)/$env(COLUMNS)))}] while {$len > 0} { lappend out [string range $line 0 [expr {$env(COLUMNS)-1}]] set line [string range $line $env(COLUMNS) end] set len [expr {$len-$env(COLUMNS)}] } } set txt [join $out "\n"] set row [expr {$n-$row}] # Reserve spaces for display: if {$end} { if {$mid} { print "[ESC]\[${mid}B" nowait } for {set x 0} {$x < $end} {incr x} { clearline print "[ESC]\[1A" nowait } } clearline set env(CMDLINE_LINES) $n # Output line(s): print "\r$txt" if {$row} { print "[ESC]\[${row}A" nowait } gotocol $col lappend env(CMDLINE_LINES) $row } proc print {txt {wait wait}} { # Sends output to stdout chunks at a time. # This is to prevent the terminal from # hanging if we output too much: while {[string length $txt]} { puts -nonewline [string range $txt 0 2047] set txt [string range $txt 2048 end] if {$wait == "wait"} { after 1 } } } rename unknown _unknown proc unknown {args} { global env ALIASES set name [lindex $args 0] set cmdline $env(CMDLINE) set cmd [string trim [regexp -inline {^\s*[^\s]+} $cmdline]] if {[info exists ALIASES($cmd)]} { set cmd [regexp -inline {^\s*[^\s]+} $ALIASES($cmd)] } set new [auto_execok $name] if {$new != ""} { set redir "" if {$name == $cmd && [info command $cmd] == ""} { set redir ">&@ stdout <@ stdin" } if {[catch { uplevel 1 exec $redir $new [lrange $args 1 end]} ret] } { return } return $ret } eval _unknown $args } proc alias {word command} { global ALIASES set ALIASES($word) $command } proc unalias {word} { global ALIASES array unset ALIASES $word } ################################ # Key bindings ################################ proc handleEscapes {} { global env upvar 1 keybuffer keybuffer set seq "" set found 0 while {[set ch [readbuf keybuffer]] != ""} { append seq $ch switch -exact -- $seq { "\[A" { ;# Cursor Up (cuu1,up) handleHistory 1 set found 1; break } "\[B" { ;# Cursor Down handleHistory -1 set found 1; break } "\[C" { ;# Cursor Right (cuf1,nd) if {$env(CMDLINE_CURSOR) < [string length $env(CMDLINE)]} { incr env(CMDLINE_CURSOR) } set found 1; break } "\[D" { ;# Cursor Left if {$env(CMDLINE_CURSOR) > 0} { incr env(CMDLINE_CURSOR) -1 } set found 1; break } "\[H" - "\[7~" - "\[1~" { ;# home set env(CMDLINE_CURSOR) 0 set found 1; break } "\[3~" { ;# delete if {$env(CMDLINE_CURSOR) < [string length $env(CMDLINE)]} { set env(CMDLINE) [string replace $env(CMDLINE) \ $env(CMDLINE_CURSOR) $env(CMDLINE_CURSOR)] } set found 1; break } "\[F" - "\[K" - "\[8~" - "\[4~" { ;# end set env(CMDLINE_CURSOR) [string length $env(CMDLINE)] set found 1; break } "\[5~" { ;# Page Up } "\[6~" { ;# Page Down } } } return $found } proc handleControls {} { global env upvar 1 char char upvar 1 keybuffer keybuffer # Control chars start at a == \u0001 and count up. switch -exact -- $char { \u0003 { ;# ^c # doExit } \u0008 - \u007f { ;# ^h && backspace ? if {$env(CMDLINE_CURSOR) > 0} { incr env(CMDLINE_CURSOR) -1 set env(CMDLINE) [string replace $env(CMDLINE) \ $env(CMDLINE_CURSOR) $env(CMDLINE_CURSOR)] } } \u001b { ;# ESC - handle escape sequences handleEscapes } } # Rate limiter: set keybuffer "" } proc shortMatch {maybe} { # Find the shortest matching substring: set maybe [lsort $maybe] set shortest [lindex $maybe 0] foreach x $maybe { while {![string match $shortest* $x]} { set shortest [string range $shortest 0 end-1] } } return $shortest } proc handleCompletion {} { global env set vars "" set cmds "" set execs "" set files "" # First find out what kind of word we need to complete: set wordstart [string last " " $env(CMDLINE) \ [expr {$env(CMDLINE_CURSOR)-1}]] incr wordstart set wordend [string first " " $env(CMDLINE) $wordstart] if {$wordend == -1} { set wordend end } else { incr wordend -1 } set word [string range $env(CMDLINE) $wordstart $wordend] if {[string trim $word] == ""} return set firstchar [string index $word 0] # Check if word is a variable: if {$firstchar == "\$"} { set word [string range $word 1 end] incr wordstart # Check if it is an array key: set x [string first "(" $word] if {$x != -1} { set v [string range $word 0 [expr {$x-1}]] incr x set word [string range $word $x end] incr wordstart $x if {[uplevel #0 "array exists $v"]} { set vars [uplevel #0 "array names $v $word*"] } } else { foreach x [uplevel #0 {info vars}] { if {[string match $word* $x]} { lappend vars $x } } } } else { # Check if word is possibly a path: if {$firstchar == "/" || $firstchar == "." || $wordstart != 0} { set files [glob -nocomplain -- $word*] } if {$files == ""} { # Not a path then get all possibilities: if {$firstchar == "\[" || $wordstart == 0} { if {$firstchar == "\["} { set word [string range $word 1 end] incr wordstart } # Check executables: foreach dir [split $env(PATH) :] { foreach f [glob -nocomplain -directory $dir -- $word*] { set exe [string trimleft [string range $f \ [string length $dir] end] "/"] if {[lsearch -exact $execs $exe] == -1} { lappend execs $exe } } } # Check commands: foreach x [info commands] { if {[string match $word* $x]} { lappend cmds $x } } } else { # Check commands anyway: foreach x [info commands] { if {[string match $word* $x]} { lappend cmds $x } } } } if {$wordstart != 0} { # Check variables anyway: set x [string first "(" $word] if {$x != -1} { set v [string range $word 0 [expr {$x-1}]] incr x set word [string range $word $x end] incr wordstart $x if {[uplevel #0 "array exists $v"]} { set vars [uplevel #0 "array names $v $word*"] } } else { foreach x [uplevel #0 {info vars}] { if {[string match $word* $x]} { lappend vars $x } } } } } set maybe [concat $vars $cmds $execs $files] set shortest [shortMatch $maybe] if {"$word" == "$shortest"} { if {[llength $maybe] > 1 && $env(COMPLETION_MATCH) != $maybe} { set env(COMPLETION_MATCH) $maybe clearline set temp "" foreach {match format} { vars "35" cmds "1;32" execs "32" files "0" } { if {[llength [set $match]]} { append temp "[ESC]\[${format}m" foreach x [set $match] { append temp "[file tail $x] " } append temp "[ESC]\[0m" } } print "\n$temp\n" } } else { if {[file isdirectory $shortest] && [string index $shortest end] != "/"} { append shortest "/" } if {$shortest != ""} { set env(CMDLINE) \ [string replace $env(CMDLINE) $wordstart $wordend $shortest] set env(CMDLINE_CURSOR) \ [expr {$wordstart+[string length $shortest]}] } elseif {$env(COMPLETION_MATCH) != " not found "} { set env(COMPLETION_MATCH) " not found " print "\nNo match found.\n" } } } proc handleHistory {x} { global env set hlen [llength $env(HISTORY)] incr env(HISTORY_LEVEL) $x if {$env(HISTORY_LEVEL) > -1} { set env(CMDLINE) [lindex $env(HISTORY) end-$env(HISTORY_LEVEL)] set env(CMDLINE_CURSOR) [string length $env(CMDLINE)] } if {$env(HISTORY_LEVEL) <= -1} { set env(HISTORY_LEVEL) -1 set env(CMDLINE) "" set env(CMDLINE_CURSOR) 0 } elseif {$env(HISTORY_LEVEL) > $hlen} { set env(HISTORY_LEVEL) $hlen } } ################################ # History handling functions ################################ proc getHistory {} { global env return $env(HISTORY) } proc setHistory {hlist} { global env set env(HISTORY) $hlist } proc appendHistory {cmdline} { global env set old [lsearch -exact $env(HISTORY) $cmdline] if {$old != -1} { set env(HISTORY) [lreplace $env(HISTORY) $old $old] } lappend env(HISTORY) $cmdline set env(HISTORY) \ [lrange $env(HISTORY) end-$env(HISTORY_BUFFER) end] } ################################ # main() ################################ proc rawInput {} { fconfigure stdin -buffering none -blocking 0 fconfigure stdout -buffering none -translation crlf exec stty raw -echo } proc lineInput {} { fconfigure stdin -buffering line -blocking 1 fconfigure stdout -buffering line exec stty -raw echo } proc doExit {{code 0}} { global env HISTFILE # Reset terminal: print "[ESC]c[ESC]\[2J" nowait lineInput set hlist [getHistory] if {[llength $hlist] > 0} { set f [open $HISTFILE w] foreach x $hlist { # Escape newlines: puts $f [string map { \n "\\n" "\\" "\\b" } $x] } close $f } exit $code } if {[file exists $RCFILE]} { source $RCFILE } # Load history if available: if {[llength $env(HISTORY)] == 0} { if {[file exists $HISTFILE]} { set f [open $HISTFILE r] set hlist [list] foreach x [split [read $f] "\n"] { if {$x != ""} { # Undo newline escapes: lappend hlist [string map { "\\n" \n "\\\\" "\\" "\\b" "\\" } $x] } } setHistory $hlist unset hlist close $f } } rawInput # This is to restore the environment on exit: # Do not unalias this! alias exit doExit proc tclline {} { global env set char "" set keybuffer [read stdin] set env(COLUMNS) [getColumns] while {$keybuffer != ""} { if {[eof stdin]} return set char [readbuf keybuffer] if {$char == ""} { # Sleep for a bit to reduce CPU time: after 40 continue } if {[string is print $char]} { set x $env(CMDLINE_CURSOR) if {$x < 1 && [string trim $char] == ""} continue set trailing [string range $env(CMDLINE) $x end] set env(CMDLINE) [string replace $env(CMDLINE) $x end] append env(CMDLINE) $char append env(CMDLINE) $trailing incr env(CMDLINE_CURSOR) } elseif {$char == "\t"} { handleCompletion } elseif {$char == "\n" || $char == "\r"} { if {[info complete $env(CMDLINE)] && [string index $env(CMDLINE) end] != "\\"} { lineInput print "\n" nowait uplevel #0 { global env ALIASES # Handle aliases: set cmdline $env(CMDLINE) set cmd [string trim [regexp -inline {^\s*[^\s]+} $cmdline]] if {[info exists ALIASES($cmd)]} { regsub -- "(?q)$cmd" $cmdline $ALIASES($cmd) cmdline } # Perform glob substitutions: set cmdline [string map { "\\*" \0 "\\~" \1 } $cmdline] # Don't substitute * and ~ in braces: foreach x [regexp -inline -all -indices {{.*?}} $cmdline] { foreach {i n} $x break set s [string range $cmdline $i $n] set s [string map { "*" \0 "~" \1 } $s] set cmdline [string replace $cmdline $i $n $s] } while {[regexp -indices \ {([\w/\.]*(?:~|\*)[\w/\.]*)+} $cmdline x] } { foreach {i n} $x break set s [string range $cmdline $i $n] set x [glob -nocomplain -- $s] # If glob can't find anything then don't do # glob substitution, pass * or ~ as literals: if {$x == ""} { set x [string map { "*" \0 "~" \1 } $s] } set cmdline [string replace $cmdline $i $n $x] } set cmdline [string map { \0 "*" \1 "~" } $cmdline] # Run the command: catch $cmdline res if {$res != ""} { print "$res\n" } # Append HISTORY: set env(HISTORY_LEVEL) -1 appendHistory $env(CMDLINE) set env(CMDLINE) "" set env(CMDLINE_CURSOR) 0 set env(CMDLINE_LINES) {0 0} } rawInput } else { set x $env(CMDLINE_CURSOR) if {$x < 1 && [string trim $char] == ""} continue set trailing [string range $env(CMDLINE) $x end] set env(CMDLINE) [string replace $env(CMDLINE) $x end] append env(CMDLINE) $char append env(CMDLINE) $trailing incr env(CMDLINE_CURSOR) } } else { handleControls } } prompt $env(CMDLINE) } tclline fileevent stdin readable tclline vwait forever doExit ====== ---- **Discussion** To have tclline automatically available when you start [tclsh] add the following to your '''.tclshrc''' file: ====== if {$tcl_interactive} { source /path/to/your/tclline.tcl } ====== [rdt] Nice idea, but my question is: Is the '''if''' test really necessary? Isn't .tclshrc only read when tclsh is running interactively? Next question: How about adding this into the interactive part of ''init.tcl'' ? [rdt] Another question: I can't really see what you changed in the ''unknown'' proc as I have 8.4 and [tkdiff] on the two procs shows a '''lot''' of change in the 8.4 version. So what did you change/add to that proc for your version? [slebetman]: Only one simple thing: I removed the [info level] and [info script] test to allow [unknown] to call [auto_execok] from any level after the shell sources tclline. Try running the commands below in an interactive session of tclsh and see if the output makes sense. If it does, that's probably the only editing you need: ====== set x [info body unknown] set x [regsub -all {if \{.{0,5}info level.{0,10}?&&} $x "if {"] set x [regsub -all {if \{.{0,5}info script.{0,10}?&&} $x "if {"] set x "proc unknown {[info args unknown]} {$x}" ====== [rdt] 2006.07.13 - With some changes, that is what I did to use the 8.4 unknown. Is there some reason that you did not use the history.tcl present in the distribution or is not present in yours? I made use of it myself. [slebetman]: I didn't know how to use it. I thought that history would always take the last executed command which also means the code in tclline itself. Now that I've tried it I see that's not really the case. But I still prefer my implementation since it removes duplicate events ''(This have been my pet peeve with almost all implementations of history, from tcl to bash to DOSKEY)''. Besides, an implementation of history is really is almost ''nothing''. It is merely an '''lappend''' to a list. [rdt] 2006.07.14 - I understand what you are saying, however: history can be used in at least two ways. Suppose that you need to redo a command from much earlier but don't remember the id. You 'history | grep ' and discover that it was 987. You can then '!987' and redo it. You do a few commands and then say '!-3' to redo it again. If you don't put duplicates in the list (or keep up with all the id's for a command), then one of these capabilities gets lost. <
> I think what you really want is to keep all the items in the list but be able to display the list in several different ways: a) 'history' for the default full list, b) 'history -20' for the 20 most recent, c) 'history -b' for the list without duplicates, but show the first & last id, and d) 'history ' to show only those items that match . [SRIV] I prefer the current optimized history. Its quick and simple and conforms to the 80/20 rule. A grep-able history is nifty, but If I need that, I shouldn't be doing work in a single console anway. I'd switch my devel work to a gui workstation. [slebetman] My implementation of history is grep-able anyway (well, [regexp]-able at least). It's just a list stored in an environment variable! foreach x $env(HISTORY) {if {[regexp $something_unique $x]} {puts $x}} The only thing I don't have is history substitution (haven't implemented because frankly I've never used it). I believe [Tkcon] does it via [unknown] which means we can implement it too. [rdt] - So you are saying with this implementation, you can't do things '!2', or '!!', or '!-2' which are already implemented in unknown? [jcolburn] - What about putting all this into a namespace? I'm taking a stab at it. Also, I added some emacs ctrl key shortcuts ====== proc handleControls {} { global env upvar 1 char char upvar 1 keybuffer keybuffer # Control chars start at a == \u0001 and count up. switch -exact -- $char { \u0001 { ;# ^a set env(CMDLINE_CURSOR) 0 } \u0002 { ;# ^b if {$env(CMDLINE_CURSOR) > 0} { incr env(CMDLINE_CURSOR) -1 } } \u0004 { ;# ^d set env(CMDLINE) [string replace $env(CMDLINE) \ $env(CMDLINE_CURSOR) $env(CMDLINE_CURSOR)] } \u0005 { ;# ^e set env(CMDLINE_CURSOR) [string length $env(CMDLINE)] } \u0006 { ;# ^f if {$env(CMDLINE_CURSOR) < [string length $env(CMDLINE)]} { incr env(CMDLINE_CURSOR) } } \u0007 { ;# ^g set env(CMDLINE) "" set env(CMDLINE_CURSOR) 0 } \u000b { ;# ^k set env(YANK) [string range $env(CMDLINE) [expr {$env(CMDLINE_CURSOR) } ] end ] set env(CMDLINE) [string range $env(CMDLINE) 0 [expr {$env(CMDLINE_CURSOR) - 1 } ]] } \u0019 { ;# ^y if { [ info exists env(YANK) ] } { set env(CMDLINE) \ "[string range $env(CMDLINE) 0 [expr {$env(CMDLINE_CURSOR) - 1 } ]]$env(YANK)[string range $env(CMDLINE) $env(CMDLINE_CURSOR) end]" } } \u000e { ;# ^n handleHistory -1 } \u0010 { ;# ^p handleHistory 1 } \u0003 { ;# ^c # doExit } \u0008 - \u007f { ;# ^h && backspace ? if {$env(CMDLINE_CURSOR) > 0} { incr env(CMDLINE_CURSOR) -1 set env(CMDLINE) [string replace $env(CMDLINE) \ $env(CMDLINE_CURSOR) $env(CMDLINE_CURSOR)] } } \u001b { ;# ESC - handle escape sequences handleEscapes } } # Rate limiter: set keybuffer "" } ====== [Bezoar] - Added the Control-Y (Yank) and updated Ctrl-K (kill to end of line ) for [emacs] bindings alsterg - Corrected a small mistake where: if {[llength $match]} { was used instead of: if {[llength [set $match]]} { [HGC] I collected the code above a few months back, found it to be quite useful - then made a few changes to my version: * put the code into a [namespace] (TclReadLine) * put the entry point into a proc (TclReadLine::interact) - so I could call it from a script (and/or call it again) * use the builtin history mechanism (so !!, !-2, etc. - would work as expected) * handle customized prompts ([tcl_prompt1] is handled - but not [tcl_prompt2]) My question: is this of interest to anyone? If so - I'd be happy to post it back here. If not - no problem. [slebetman] The usual rule of the wiki is: go ahead and post it. I'd suggest creating a new page [Pure-tcl readline2] (like how I did with the pure tcl text editor) and post your code there. I'd definitely be interested in it being namespaced properly (the next step is to run commands in a slave interp - like how [tkcon] does it - but that will have to wait for now). <
> [HGC] Done. Note that there is some dead code in the new version; I didn't completely remove the previous '[history]' implementation, for example. (Further cleanup is possible.) Further isolation into a slave [interp] is a good idea. [ZB] 2010-01-03 If tclline.tcl can replace the "usual" bash (or whichever other shell), I would to make a feature suggestion, that could help us to dispose of all that "[exec magic]": I think, it could be useful to equip it with ability to recognize "on its own" proper interpreter, looking just at the file extension. Five conditions should be met to trigger such behaviour: if the file has "eXecute" flag set, if the filename has any extension at all, if it's "pure text" file, if there isn't in its first line interpreter choosen explicitly already, and if the interpreter for the files of the given extension is present in the system installation. For example: * all *.pl files could have default interpreter `which perl` * all *.tcl files could have default interpreter `which tclsh` * all *.c files could have default interpreter `which tcc` (TCC compiler will treat C files as scripts) * ...and so on for Python, Ruby, Lua and many other scripting languages. Actually, why it shouldn't figure out by itself: "the file has .tcl extension, so obviously we need tclsh to execute" - quite on its own? ---- [Category Dev. Tools]