├── .gitignore ├── .tcllinerc.sample ├── LICENSE.txt ├── README.md ├── TclReadLine ├── TclReadLine.tcl ├── help.txt └── pkgIndex.tcl ├── changelog.txt └── tcl.sample /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .*-version 3 | .DS_Store 4 | .swp 5 | .orig 6 | Session.vim 7 | tags 8 | memo 9 | 10 | -------------------------------------------------------------------------------- /.tcllinerc.sample: -------------------------------------------------------------------------------- 1 | TclReadLine::alias l {puts "\\033\\[35m===========================================\\033\\[0m"} 2 | TclReadLine::alias p puts 3 | TclReadLine::alias c clear 4 | TclReadLine::alias %d {TclReadLine::dumpLog} 5 | TclReadLine::alias %a {TclReadLine::alias} 6 | TclReadLine::alias %u {TclReadLine::unalias} 7 | 8 | TclReadLine::keyword {TclReadLine::} 9 | 10 | TclReadLine::keyword {string cat} 11 | TclReadLine::keyword {string compare} 12 | TclReadLine::keyword {string equal} 13 | TclReadLine::keyword {string first} 14 | TclReadLine::keyword {string index} 15 | TclReadLine::keyword {string is} 16 | TclReadLine::keyword {string last} 17 | TclReadLine::keyword {string length} 18 | TclReadLine::keyword {string map} 19 | TclReadLine::keyword {string match} 20 | TclReadLine::keyword {string range} 21 | TclReadLine::keyword {string repeat} 22 | TclReadLine::keyword {string replace} 23 | TclReadLine::keyword {string reverse} 24 | TclReadLine::keyword {string tolower} 25 | TclReadLine::keyword {string totitle} 26 | TclReadLine::keyword {string toupper} 27 | TclReadLine::keyword {string trim} 28 | TclReadLine::keyword {string trimleft} 29 | TclReadLine::keyword {string trimright} 30 | 31 | TclReadLine::keyword {tcl_startOfNextWord} 32 | TclReadLine::keyword {tcl_wordBreakAfter} 33 | TclReadLine::keyword {tcl_startOfPreviousWord} 34 | TclReadLine::keyword {tcl_wordBreakBefore} 35 | TclReadLine::keyword {tcl_endOfWord} 36 | 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### :coffee: Tclsh-Wrapper 2 | 3 | **Tclsh-Wrapper** is a tiny wrapper for **tclsh** ( Shell/interpretor application for [Tcl](https://www.tcl.tk) programming language ) 4 | 5 | ##### :tea: Motivation 6 | Most shell applications (Bash, Python, etc) have command history navigation feature (mostly invoked by pressing UP key). However, `tclsh` offers no such basic functionality. It doesn't even understand UP/DOWN key press!!! 7 | 8 | That is not a big problem since [Tcl/Tk](https://www.tcl.tk) package ships with an alternative shell application **tkcon**. It's a GUI shell, and much better. 9 | 10 | But still, if `tclsh` can offer command history navigation, I'll prefer it to `tkcon` since it's much more productive and convenient to terminal geeks anyway. 11 | 12 | After a bit of research, I found [this code](http://wiki.tcl.tk/20215) which offers what I needed. I changed some code in it and added a few new convenient features and that is **Tclsh-Wrapper**. 13 | 14 | ##### :tea: What have been changed 15 | 16 | - Removed a dependency to some 3rd party package 17 | - I guess the original intent was supporting cross platform compatibility, but when you use `tclsh` only in Unix like systems, that 3rd party package was a complete overkill. 18 | - Added convenient cursor movement from word to word 19 | - Added convenient glob search for command history 20 | - Colorized the prompt, error messages, etc. 21 | - Changed some (seemingly) inefficient code 22 | 23 | ### :coffee: How To Use It 24 | 25 | Assume you copy the package to `~/lib/tcl`. (Of course, you can copy it anywhere you like) 26 | 27 | 1. Clone the package 28 | - `git clone https://github.com/suewonjp/tclsh-wrapper.git ~/lib/tcl/tclsh-wrapper` 29 | 1. Create a launcher script like so: 30 | 31 | touch tcl; chmod a+x tcl; echo " 32 | #!/bin/sh 33 | exec tclsh ~/lib/tcl/tclsh-wrapper/TclReadLine/TclReadLine.tcl 34 | " > tcl 35 | 1. Copy the generated `tcl` script somewhere in the system path. 36 | 1. Now execute `tcl` instead of `tclsh` 37 | - Notice that the `tcl` script above is intended for interactive uses only and not a complete replacement for `tclsh`. 38 | - You still need to use `tclsh` for executing Tcl scripts. 39 | - See the [sample launcher script](tcl.sample) 40 | 41 | Alternatively, create a `~/.tclshrc` file and append the code as follows: 42 | 43 | lappend auto_path "~/lib/tcl/tclsh-wrapper" 44 | 45 | if {$::tcl_interactive} { 46 | package require TclReadLine 47 | TclReadLine::interact 48 | } 49 | In this case, you can run `tclsh` directly, then it will read `~/.tclshrc` at startup and eventually load **Tclsh-Wrapper**. 50 | 51 | > Once **Tclsh-Wrapper** invoked, Type `help` to print out all key bindings and additional help messages. 52 | 53 | ### :coffee: Key Bindings 54 | 55 | - CONTROL + a / HOME 56 | - Move the cursor to the start of the line 57 | - CONTROL + b / LEFT 58 | - Move the cursor to the previous character 59 | - CONTROL + d 60 | - Terminate the shell. Same as executing `exit` command. 61 | - CONTROL + e / END 62 | - Move the cursor to the end of the line 63 | - CONTROL + f / RIGHT 64 | - Move the cursor to the next character 65 | - CONTROL + g 66 | - Clear the entire line 67 | - CONTROL + k 68 | - Clear everything from the cursor to the end of the line 69 | - And the cleared text will be YANKED (remembered) 70 | - CONTROL + y 71 | - Paste the text previously yanked 72 | - CONTROL + n / DOWN 73 | - Retrieve an older command line from the command history 74 | - CONTROL + p / UP 75 | - Retrieve a newer command line from the command history 76 | - CONTROL + u 77 | - Clear everything from the start of the line to the cursor 78 | - And the cleared text will be YANKED (remembered) 79 | - CONTROL + w 80 | - Delete the previous word 81 | - CONTROL + h / Backspace 82 | - Delete the previous character 83 | - TAB 84 | - Keyword completion for commands, variable, file path, etc. 85 | 86 | > Depending on terminals you're using, **Tclsh-Wrapper** may not recognize ALT key press. Read the topic of **ALT key limitation** below for more detail 87 | 88 | - ALT + 0 89 | - Move the cursor to the start of the line. Same as CONTROL + a 90 | - ALT + b 91 | - Move to the start of the word 92 | - ALT + d 93 | - Delete the next word 94 | - ALT + e 95 | - Move to the end of the word 96 | - ALT + f / ALT + w 97 | - Move to the start of the next word 98 | - ALT + / 99 | - Clear the entire line and insert `?` at the start of the line. 100 | - This will quickly let you search commands with glob patterns through the command history 101 | - Read *History Search Mode* below for more detail 102 | - ALT + k 103 | - Retrieve an older command line from the command history 104 | - Same as CONTROL + p or UP 105 | - ALT + j 106 | - Retrieve a newer command line from the command history 107 | - Same as CONTROL + n or DOWN 108 | - ALT + h 109 | - Move the cursor to the previous character 110 | - Same as CONTROL + b or LEFT 111 | - ALT + l 112 | - Move the cursor to the next character 113 | - Same as CONTROL + f or RIGHT 114 | - ALT + x / DELETE 115 | - Delete the next character 116 | - Shift + LEFT 117 | - Move the cursor to the previous blank 118 | - Shift + RIGHT 119 | - Move the cursor to the next blank 120 | 121 | ### :coffee: Commands 122 | - exit 123 | - Exit **Tclsh-Wrapper** 124 | - quit 125 | - Same as exit 126 | - help 127 | - Print out help messages 128 | - clear 129 | - Clear the screen 130 | - keyword 131 | - Add an arbitrary user keyword which will be expanded on keyword completion 132 | - See **Keyword Completion** section below 133 | - alias 134 | - Define an alias for an arbitrary command 135 | - See **Command Aliases** section below 136 | - unalias 137 | - Undefine an aliase previously defined 138 | 139 | ### :coffee: History Search Mode 140 | You can navigate the command history one by one by typing `UP/CONTROL+p/ALT+k` (retrieving older command lines) or `DOWN/CONTROL+n/ALT+j` (retrieving newer command lines). 141 | 142 | But sometimes this may be so tedious. When you know some word contained in your target command line, you can quickly retrieve it by using that word. 143 | 144 | For example, type `?foo`(question mark + [your search word]) and press `ENTER`. **Tclsh-Wrapper** will quickly retrieve the most recent command line containing `foo`. Also this will let you in a special mode called **History Search Mode**. Remember, in **History Search Mode**, `UP/CONTROL+p/ALT+k` and `DOWN/CONTROL+n/ALT+j` behave a little bit differently. When you press `UP` in the previous example, **Tclsh-Wrapper** will retrieve one step older command line containing `foo` if any. 145 | 146 | In **History Search Mode**, navigation will be restricted to only command lines containing the search word. 147 | 148 | The **History Search Mode** will end as soon as you execute some command. To manually end the **History Search Mode** without executing any command, type `CONTROL+g` and `ENTER`. ( Alternatively, type `ALT+/` and `ENTER` ) 149 | 150 | ### :coffee: How to Customize it 151 | 152 | Most of these features were already there at the [base code](http://wiki.tcl.tk/20215) and quite useful. 153 | 154 | ##### :tea: Configuration File 155 | **Tclsh-Wrapper** reads a configuration file if it discovers one at startup and executes arbitrary Tcl commands in that file. 156 | 157 | The path to the file is `~/.tcllinerc` by default, but you can change the path by assigning a new path to **TCLLINERC** environment variable. 158 | 159 | ##### :tea: Command Aliases 160 | If you find yourself typing same command lines over and over, then aliases may help you. 161 | 162 | > TclReadLine::alias p puts 163 | > p {hello} 164 | hello 165 | Aliases defined inside an interactive session will be valid only in that session. You can write aliases in your configuration file (`~/.tclshrc`) and make them persistent. 166 | 167 | Also there is a `TclReadLine::unalias` command so you can remove them. 168 | 169 | > You should use command aliases at the start of command line. Aliases in anywhere else won't be recognized 170 | 171 | > Also when the aliased command has whitespaces in them, you should quote it with braces (See examples below) 172 | 173 | **Useful Aliases:** 174 | 175 | ### Print a long horizontal line in magenta color 176 | TclReadLine::alias l {puts "\\033\\[35m===========================================\\033\\[0m"} 177 | 178 | ### I'm kind of lazy so I'd like to type `p` instead of `puts` 179 | TclReadLine::alias p puts 180 | 181 | ### Alias on the fly (e.g., %a iii {info globals}) 182 | TclReadLine::alias %a {TclReadLine::alias} 183 | 184 | ### Unalias on the fly (e.g., %u iii) 185 | TclReadLine::alias %u {TclReadLine::unalias} 186 | 187 | ##### :tea: Keyword Completion 188 | Simple keyword completion is provided. For example, `fore` will expand to `foreach`. 189 | 190 | By default, you can expand patterns if that matches either of the following: 191 | 192 | - Variables 193 | - Tcl/Tk commands 194 | - Executables in the system path 195 | - File path 196 | 197 | Understand that you may not match every Tcl/Tk command. ( It seems like a limitation of Tcl interpretor rather than **Tclsh-Wrapper** ). 198 | 199 | You can match Ensemble commands such as `string`, `array`, etc. (Since version 1.4) 200 | 201 | Also, you can extend the list of match candidates using `TclReadLine::keyword` API. For example, inserting the line `TclReadLine::keyword {TclReadLine::}` in the configuration file will let you type just `TclR` for `TclReadLine::`. 202 | 203 | For another example, inserting the following lines into the configuration file will let you just type `s*tol` for `string tolower`. 204 | 205 | TclReadLine::keyword {string tolower} 206 | TclReadLine::keyword {string totitle} 207 | TclReadLine::keyword {string toupper} 208 | TclReadLine::keyword {string trim} 209 | TclReadLine::keyword {string trimleft} 210 | TclReadLine::keyword {string trimright} 211 | 212 | However, `s*trim` will print out 3 candidates (`string trim`, `string trimleft`, `string trimright`). If your target were `string trimleft`, then what you do is just appending `l` and type ``. What if your target were `string trim`? Then, type `s*trim$`. `$` notation at the end will match words ending with the character before that `$`. 213 | 214 | > See the [sample .tcllinerc file](.tcllinerc.sample) 215 | 216 | ##### :tea: History File Path 217 | **Tclsh-Wrapper** persists the command history records to a file. (`~/tclline_history` by default) 218 | 219 | You can change the path by assigning a new path to **TCLLINE_HISTORY** environment variable. 220 | 221 | This may be useful when you want to maintain separate command history for `tclsh` and `wish`. In my opinion, sharing the same command history for both shells is not a good idea since `tclsh` doesn't understand Tk commands by default and when using `tclsh` after using `wish`, the command history would be polluted with a bunch of commands that `tclsh` can't even execute. 222 | 223 | For example, you can change the launcher script (named `tcl`) introduced earlier like so: 224 | 225 | #!/bin/sh 226 | 227 | if [ "$1" = "w" ]; then 228 | TCLLINE_HISTORY=~/.tclline_history_w exec wish ~/test/trysomething/tclsh-wrapper/TclReadLine/TclReadLine.tcl 229 | else 230 | TCLLINE_HISTORY=~/.tclline_history exec tclsh ~/test/trysomething/tclsh-wrapper/TclReadLine/TclReadLine.tcl 231 | fi 232 | 233 | You can invoke `tclsh` as before by running `tcl`, and invoke `wish` by running `tcl w`. Notice that `~/.tclline_history` will be used for `tclsh` sessions, and `~/.tclline_history_w` will be used for `wish` sessions. 234 | 235 | ### :coffee: Issues 236 | 237 | - This package will work for most of Unix like systems (Linux, Mac OS X, Cygwin, etc), but it doesn't support Windows for now. 238 | - I mean `tclsh.exe`, the Windows executable, not `tclsh` on Cygwin 239 | - You may notice that the command line cursor flickers at times when it moves fast. But it's not a problem in terms of functionalities and you can safely ignore it. 240 | - ALT key limitation 241 | - **Tclsh-Wrapper** assumes your terminal handles ALT keys in a *ESC prefixed* way 242 | - When you're not sure, execute `cat -v` and type `ALT+a` (Press `ALT` and `a` together) 243 | - When the terminal prints something like `^[a`, then you're OK. 244 | - If your terminal prints something different, **Tclsh-Wrapper** may not recognize key bindings with ALT key press. 245 | - Many terminals offer an option for you to switch how it handles ALT key press. Consult the help or manual of your terminal. 246 | - Support for file path containing spaces 247 | - Since version 1.4 248 | - File path containing spaces can be recognized only when you prepend a backslash in front of the space like `~/Google\ Drive`. 249 | - As a limitation, no other method is supported 250 | - **Tclsh-Wrapper** can't recognize quoted path with single/double quotation marks. 251 | - Thus, don't expect quoting path containing spaces would work. Only prepending backslash will work. 252 | 253 | 254 | ### :copyright: COPYRIGHT/LICENSE/DISCLAIMER 255 | 256 | Copyright (c) 2018 Suewon Bahng, suewonjp@gmail.com 257 | 258 | Licensed under the Apache License, Version 2.0 (the "License"); 259 | you may not use this file except in compliance with the License. 260 | You may obtain a copy of the License at 261 | 262 | http://www.apache.org/licenses/LICENSE-2.0 263 | 264 | Unless required by applicable law or agreed to in writing, software 265 | distributed under the License is distributed on an "AS IS" BASIS, 266 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 267 | See the License for the specific language governing permissions and 268 | limitations under the License. 269 | 270 | ### :busts_in_silhouette: CONTRIBUTORS 271 | Suewon Bahng 272 | 273 | * * * 274 | Updated by Suewon Bahng ( Jan 2018 ) 275 | 276 | -------------------------------------------------------------------------------- /TclReadLine/TclReadLine.tcl: -------------------------------------------------------------------------------- 1 | # 2 | # You can find the base source for this script at http://wiki.tcl.tk/20215 3 | # 4 | # Read README.md included in the package for detail 5 | # 6 | 7 | package provide TclReadLine 1.4 8 | 9 | namespace eval TclReadLine { 10 | 11 | namespace export interact 12 | 13 | # Initialise our own env variables: 14 | variable PROMPT {[ESC]\[36m> [ESC]\[0m} 15 | 16 | # Support extensions to the completion handling 17 | # which will be called in list order. 18 | # Initialize with the "open sourced" TCL base handler 19 | # taken from the wiki page 20 | #variable COMPLETION_HANDLERS [list TclReadLine::handleCompletionBase] 21 | 22 | variable USER_KEYWORDS [] 23 | 24 | # 25 | # This value was determined by measuring 26 | # a cygwin over ssh. 27 | # 28 | variable READLINE_LATENCY 10 ;# in ms 29 | 30 | variable CMDLINE "" 31 | variable CMDLINE_CURSOR 0 32 | variable CMDLINE_LINES 0 33 | variable CMDLINE_PARTIAL 34 | 35 | variable ALIASES 36 | array set ALIASES {} 37 | 38 | # Context data for the history search 39 | variable HSEARCH_STATE OFF 40 | variable HSEARCH_LIST [list] 41 | variable HSEARCH_INDICES [list] 42 | variable HSEARCH_WORD {} 43 | 44 | variable forever 0 45 | 46 | # Resource and history files: 47 | variable HISTORY_SIZE 100 48 | variable HISTORY_LEVEL 0 49 | variable HISTFILE $::env(HOME)/.tclline_history 50 | variable RCFILE $::env(HOME)/.tcllinerc 51 | 52 | # Context data for multiline mode 53 | variable MULTILINE_LIST [list] 54 | } 55 | 56 | namespace path {::tcl::mathop ::tcl::mathfunc ::tcl::} 57 | 58 | # Debug purpose utils 59 | if 0 { 60 | proc TclReadLine::log {value} { 61 | variable LOG_MSGS 62 | lappend LOG_MSGS $value 63 | } 64 | 65 | proc TclReadLine::dumpLog {} { 66 | variable LOG_MSGS 67 | if {[info exists LOG_MSGS]} { 68 | puts [join $LOG_MSGS "\n"] 69 | } 70 | } 71 | } 72 | 73 | proc TclReadLine::help {} { 74 | set helpfile [file join [file dirname $::argv0] help.txt] 75 | set f [open $helpfile] 76 | puts "[ESC]\[33m[read $f][ESC]\[0m" 77 | close $f 78 | } 79 | 80 | proc TclReadLine::ESC {} { 81 | return "\033" 82 | } 83 | 84 | #proc TclReadLine::shift {ls} { 85 | #upvar 1 $ls LIST 86 | #set ret [lindex $LIST 0] 87 | #set LIST [lrange $LIST 1 end] 88 | #return $ret 89 | #} 90 | 91 | proc TclReadLine::readbuf {txt} { 92 | upvar 1 $txt STRING 93 | 94 | set ret [string index $STRING 0] 95 | set STRING [string range $STRING 1 end] 96 | return $ret 97 | } 98 | 99 | proc TclReadLine::goto {row {col 1}} { 100 | switch -- $row { 101 | "home" {set row 1} 102 | } 103 | print "[ESC]\[${row};${col}H" nowait 104 | } 105 | 106 | proc TclReadLine::gotocol {col} { 107 | print "\r" nowait 108 | if {$col > 0} { 109 | print "[ESC]\[${col}C" nowait 110 | } 111 | } 112 | 113 | proc TclReadLine::clear {} { 114 | print "[ESC]\[2J" nowait 115 | goto home 116 | } 117 | 118 | proc TclReadLine::clearline {} { 119 | print "[ESC]\[2K\r" nowait 120 | } 121 | 122 | proc TclReadLine::getColumns {} { 123 | variable COLUMNS 124 | set cols 0 125 | if {![catch {exec stty size} size]} { 126 | lassign $size rows cols 127 | } elseif {![catch {exec stty -a} err]} { 128 | # check for Linux style stty output 129 | if {[regexp {rows (= )?(\d+); columns (= )?(\d+)} $err - i1 rows i2 cols]} { 130 | return [set COLUMNS $cols] 131 | } 132 | # check for BSD style stty output 133 | if {[regexp { (\d+) rows; (\d+) columns;} $err - rows cols]} { 134 | return [set COLUMNS $cols] 135 | } 136 | } 137 | set COLUMNS $cols 138 | } 139 | 140 | proc TclReadLine::localInfo {args} { 141 | set v [uplevel _info $args] 142 | if { [string equal "script" [lindex $args 0]] } { 143 | if { [string equal $v $TclReadLine::ThisScript] } { 144 | return "" 145 | } 146 | } 147 | return $v 148 | } 149 | 150 | proc TclReadLine::localPuts {args} { 151 | 152 | set l [llength $args] 153 | if { 3 < $l } { 154 | return -code error "Error: wrong \# args" 155 | } 156 | 157 | if { 1 < $l } { 158 | if { [string equal "-nonewline" [lindex $args 0]] } { 159 | if { 2 < $l } { 160 | # we don't send to channel... 161 | eval _origPuts $args 162 | } else { 163 | set str [lindex $args 1] 164 | append TclReadLine::putsString $str ;# no newline... 165 | } 166 | } else { 167 | # must be a channel 168 | eval _origPuts $args 169 | } 170 | } else { 171 | append TclReadLine::putsString [lindex $args 0] "\n" 172 | } 173 | } 174 | 175 | proc TclReadLine::prompt {{txt ""} {pr ""}} { 176 | 177 | if { "" != [info var ::tcl_prompt1] } { 178 | rename ::puts ::_origPuts 179 | rename TclReadLine::localPuts ::puts 180 | variable putsString 181 | set putsString "" 182 | eval [set ::tcl_prompt1] 183 | set prompt $putsString 184 | rename ::puts TclReadLine::localPuts 185 | rename ::_origPuts ::puts 186 | } else { 187 | variable PROMPT 188 | if { $pr != "" } { 189 | set prompt [subst $pr] 190 | } else { 191 | set prompt [subst $PROMPT] 192 | } 193 | } 194 | set txt "$prompt$txt" 195 | variable CMDLINE_LINES 196 | variable CMDLINE_CURSOR 197 | variable COLUMNS 198 | foreach {end mid} $CMDLINE_LINES break 199 | 200 | # Calculate how many extra lines we need to display. 201 | # Also calculate cursor position: 202 | set n -1 203 | set totalLen 0 204 | set visprompt [regsub -all {\x1b\[[0-9;]*[a-zA-Z]} $prompt {}] ;# strip colour codes 205 | set cursorLen [expr {$CMDLINE_CURSOR+[string length $visprompt]}] 206 | set row 0 207 | set col 0 208 | 209 | # Render output line-by-line to $out then copy back to $txt: 210 | set found 0 211 | set out [list] 212 | foreach line [split $txt "\n"] { 213 | set len [expr {[string length $line]+1}] 214 | incr totalLen $len 215 | if {$found == 0 && $totalLen >= $cursorLen} { 216 | set cursorLen [expr {$cursorLen - ($totalLen - $len)}] 217 | set col [expr {$cursorLen % $COLUMNS}] 218 | set row [expr {$n + ($cursorLen / $COLUMNS) + 1}] 219 | 220 | if {$cursorLen >= $len} { 221 | set col 0 222 | incr row 223 | } 224 | set found 1 225 | } 226 | incr n [expr {int(ceil(double($len)/$COLUMNS))}] 227 | while {$len > 0} { 228 | lappend out [string range $line 0 [expr {$COLUMNS-1}]] 229 | set line [string range $line $COLUMNS end] 230 | set len [expr {$len-$COLUMNS}] 231 | } 232 | } 233 | set txt [join $out "\n"] 234 | set row [expr {$n-$row}] 235 | 236 | # Reserve spaces for display: 237 | if {$end} { 238 | if {$mid} { 239 | print "[ESC]\[${mid}B" nowait 240 | } 241 | for {set x 0} {$x < $end} {incr x} { 242 | clearline 243 | print "[ESC]\[1A" nowait 244 | } 245 | } 246 | clearline 247 | set CMDLINE_LINES $n 248 | 249 | # Output line(s): 250 | print "\r$txt" 251 | 252 | if {$row} { 253 | print "[ESC]\[${row}A" nowait 254 | } 255 | gotocol $col 256 | lappend CMDLINE_LINES $row 257 | } 258 | 259 | proc TclReadLine::print {txt {wait wait}} { 260 | # Sends output to stdout chunks at a time. 261 | # This is to prevent the terminal from 262 | # hanging if we output too much: 263 | while {[string length $txt]} { 264 | puts -nonewline [string range $txt 0 2047] 265 | set txt [string range $txt 2048 end] 266 | if {$wait == "wait"} { 267 | after 1 268 | } 269 | } 270 | } 271 | 272 | proc TclReadLine::unknown {args} { 273 | 274 | set name [lindex $args 0] 275 | set cmdline $TclReadLine::CMDLINE 276 | set cmd [string trim [regexp -inline {^\s*[^\s]+} $cmdline]] 277 | if {[info exists TclReadLine::ALIASES($cmd)]} { 278 | set cmd [regexp -inline {^\s*[^\s]+} $TclReadLine::ALIASES($cmd)] 279 | } 280 | 281 | set new [auto_execok $name] 282 | if {$new != ""} { 283 | set redir "" 284 | if {$name == $cmd && [info command $cmd] == ""} { 285 | set redir ">&@ stdout <@ stdin" 286 | } 287 | if {[catch { 288 | uplevel 1 exec $redir $new [lrange $args 1 end]} ret] 289 | } { 290 | return 291 | } 292 | return $ret 293 | } 294 | 295 | uplevel _unknown $args 296 | } 297 | 298 | proc TclReadLine::alias {word command} { 299 | variable ALIASES 300 | set ALIASES($word) $command 301 | } 302 | 303 | proc TclReadLine::unalias {word} { 304 | variable ALIASES 305 | array unset ALIASES $word 306 | } 307 | 308 | proc TclReadLine::getCmdlineToCursor {{trim 0}} { 309 | variable CMDLINE 310 | variable CMDLINE_CURSOR 311 | set str [string range $CMDLINE 0 [expr $CMDLINE_CURSOR - 1]] 312 | if {$trim} { 313 | set str [string trimright $str] 314 | } 315 | return $str 316 | } 317 | 318 | proc TclReadLine::setHistorySearchState {state} { 319 | variable HSEARCH_STATE 320 | set HSEARCH_STATE $state 321 | } 322 | 323 | proc TclReadLine::processHistorySearch {searchWord {direction 1}} { 324 | variable HSEARCH_STATE 325 | variable HSEARCH_LIST 326 | variable HSEARCH_INDICES 327 | variable HSEARCH_WORD 328 | switch -exact -- $HSEARCH_STATE { 329 | OFF { 330 | } 331 | SWITCH_ON { 332 | # The list for the command history in reverse order 333 | set HSEARCH_LIST [lreverse [getHistory]] 334 | # The list for the index of the matched command 335 | set HSEARCH_INDICES [list -1] 336 | # The current search word 337 | set HSEARCH_WORD $searchWord 338 | 339 | setHistorySearchState ON 340 | tailcall processHistorySearch $searchWord 1 341 | } 342 | ON { 343 | if {$direction == 1} { ;# Cursor up 344 | if {$searchWord == {}} { 345 | set searchWord $HSEARCH_WORD 346 | } 347 | 348 | # When the search starts, this variable is assigned 0 349 | # Otherwise, it is assigned [the index of the last retrieved command] + 1 350 | set startIdx [expr [lindex $HSEARCH_INDICES end] + 1] 351 | 352 | set idx [lsearch -start $startIdx -nocase $HSEARCH_LIST "*$searchWord*"] 353 | if {$idx == -1} { ;# No match found 354 | if {[llength $HSEARCH_INDICES] == 1} { 355 | # It was a first time try, 356 | # which means there is no match for the entire history records 357 | setHistorySearchState SWITCH_OFF 358 | tailcall processHistorySearch $searchWord 1 359 | } 360 | # Returns the last command found 361 | return [lindex $HSEARCH_LIST [lindex $HSEARCH_INDICES end]] 362 | } else { 363 | set lastCmd [lindex $HSEARCH_LIST [lindex $HSEARCH_INDICES end]] 364 | if {$lastCmd == [lindex $HSEARCH_LIST $idx]} { 365 | # The new match is identical with the last match. 366 | # Retry search until we find a different command 367 | set HSEARCH_INDICES [lreplace $HSEARCH_INDICES end end $idx] 368 | tailcall processHistorySearch $searchWord 1 369 | } 370 | } 371 | # Append the index of the new match to the match list 372 | set HSEARCH_INDICES [lappend HSEARCH_INDICES $idx] 373 | } else { ;# Cursor down 374 | if {[llength $HSEARCH_INDICES] > 1} { 375 | # Remove the index at the top of the match list 376 | set HSEARCH_INDICES [lreplace $HSEARCH_INDICES end end] 377 | } 378 | set idx [lindex $HSEARCH_INDICES end] 379 | } 380 | return [lindex $HSEARCH_LIST $idx] 381 | } 382 | SWITCH_OFF { 383 | set HSEARCH_LIST [list] 384 | set HSEARCH_INDICES [list] 385 | set HSEARCH_WORD {} 386 | setHistorySearchState OFF 387 | } 388 | } 389 | return {} 390 | } 391 | 392 | # Key bindings 393 | proc TclReadLine::handleEscapes {} { 394 | 395 | variable CMDLINE 396 | variable CMDLINE_CURSOR 397 | 398 | upvar 1 keybuffer keybuffer 399 | set seq "" 400 | set found 0 401 | while {[set ch [readbuf keybuffer]] != ""} { 402 | append seq $ch 403 | 404 | switch -exact -- $seq { 405 | "b" { ;# Move to the previous word 406 | set CMDLINE_CURSOR [tcl_startOfPreviousWord $CMDLINE $CMDLINE_CURSOR] 407 | } 408 | "e" { ;# Move to the end of the current word 409 | set dstCursor [tcl_endOfWord $CMDLINE $CMDLINE_CURSOR] 410 | if {$dstCursor < 0} { 411 | set dstCursor [string length $CMDLINE] 412 | } 413 | set CMDLINE_CURSOR $dstCursor 414 | } 415 | "f" - 416 | "w" { ;# Move to the next word 417 | set dstCursor [tcl_startOfNextWord $CMDLINE $CMDLINE_CURSOR] 418 | if {$dstCursor < 0} { 419 | set dstCursor [string length $CMDLINE] 420 | } 421 | set CMDLINE_CURSOR $dstCursor 422 | } 423 | "d" { ;# Delete the next word 424 | set dstCursor [tcl_endOfWord $CMDLINE $CMDLINE_CURSOR] 425 | if {$dstCursor < 0} { 426 | set dstCursor [string length $CMDLINE] 427 | } 428 | set CMDLINE [string replace $CMDLINE $CMDLINE_CURSOR $dstCursor] 429 | } 430 | "/" { 431 | set CMDLINE {?} 432 | set CMDLINE_CURSOR 1 433 | } 434 | "0" { 435 | set CMDLINE_CURSOR 0 436 | } 437 | "k" - 438 | "\[A" { ;# Cursor Up (cuu1,up) 439 | handleHistory 1 440 | set found 1; break 441 | } 442 | "j" - 443 | "\[B" { ;# Cursor Down 444 | handleHistory -1 445 | set found 1; break 446 | } 447 | "l" - 448 | "\[C" { ;# Cursor Right (cuf1,nd) 449 | if {$CMDLINE_CURSOR < [string length $CMDLINE]} { 450 | incr CMDLINE_CURSOR 451 | } 452 | set found 1; break 453 | } 454 | "h" - 455 | "\[D" { ;# Cursor Left 456 | if {$CMDLINE_CURSOR > 0} { 457 | incr CMDLINE_CURSOR -1 458 | } 459 | set found 1; break 460 | } 461 | "\[1;2C" { ;# Shift + Right ( Move to the next blank ) 462 | regexp -start $CMDLINE_CURSOR -indices {\S} $CMDLINE matchedRange 463 | if {[info exists matchedRange] && [llength $matchedRange]} { 464 | regexp -start [lindex $matchedRange 0] -indices {\s} $CMDLINE matchedRange2 465 | if {[info exists matchedRange2] && [llength $matchedRange2]} { 466 | set CMDLINE_CURSOR [lindex $matchedRange2 0] 467 | } else { 468 | set CMDLINE_CURSOR [string length $CMDLINE] 469 | } 470 | } 471 | } 472 | "\[1;2D" { ;# Shift + Left ( Move to the previous blank ) 473 | set dstCursor [string last " " [string trimright [getCmdlineToCursor]]] 474 | if { $dstCursor == -1 } { 475 | set dstCursor 0 476 | } 477 | set CMDLINE_CURSOR $dstCursor 478 | } 479 | "\[H" - 480 | "\[7~" - 481 | "\[1~" { ;# home 482 | set CMDLINE_CURSOR 0 483 | set found 1; break 484 | } 485 | "x" - 486 | "\[3~" { ;# delete 487 | if {$CMDLINE_CURSOR < [string length $CMDLINE]} { 488 | set CMDLINE [string replace $CMDLINE \ 489 | $CMDLINE_CURSOR $CMDLINE_CURSOR] 490 | } 491 | set found 1; break 492 | } 493 | "\[F" - 494 | "\[K" - 495 | "\[8~" - 496 | "\[4~" { ;# end 497 | set CMDLINE_CURSOR [string length $CMDLINE] 498 | set found 1; break 499 | } 500 | "\[5~" { ;# Page Up 501 | } 502 | "\[6~" { ;# Page Down 503 | } 504 | } 505 | } 506 | return $found 507 | } 508 | 509 | proc TclReadLine::handleControls {} { 510 | 511 | variable CMDLINE 512 | variable CMDLINE_CURSOR 513 | 514 | upvar 1 char char 515 | upvar 1 keybuffer keybuffer 516 | 517 | # Control chars start at a == \u0001 and count up. 518 | switch -exact -- $char { 519 | \u0001 { ;# ^a 520 | set CMDLINE_CURSOR 0 521 | } 522 | \u0002 { ;# ^b 523 | if { $CMDLINE_CURSOR > 0 } { 524 | incr CMDLINE_CURSOR -1 525 | } 526 | } 527 | \u0004 { ;# ^d 528 | print "\n" nowait 529 | doExit 530 | } 531 | \u0005 { ;# ^e 532 | set CMDLINE_CURSOR [string length $CMDLINE] 533 | } 534 | \u0006 { ;# ^f 535 | if {$CMDLINE_CURSOR < [string length $CMDLINE]} { 536 | incr CMDLINE_CURSOR 537 | } 538 | } 539 | \u0007 { ;# ^g 540 | set CMDLINE "" 541 | set CMDLINE_CURSOR 0 542 | } 543 | \u000b { ;# ^k 544 | variable YANK 545 | set YANK [string range $CMDLINE [expr {$CMDLINE_CURSOR } ] end ] 546 | set CMDLINE [string range $CMDLINE 0 [expr {$CMDLINE_CURSOR - 1 } ]] 547 | } 548 | \u0019 { ;# ^y 549 | variable YANK 550 | if { [ info exists YANK ] } { 551 | set CMDLINE \ 552 | "[string range $CMDLINE 0 [expr {$CMDLINE_CURSOR - 1 }]]$YANK[string range $CMDLINE $CMDLINE_CURSOR end]" 553 | } 554 | } 555 | \u000e { ;# ^n 556 | handleHistory -1 557 | } 558 | \u0010 { ;# ^p 559 | handleHistory 1 560 | } 561 | \u0015 { ;# ^u 562 | variable YANK 563 | set YANK [string range $CMDLINE 0 [expr $CMDLINE_CURSOR-1]] 564 | set CMDLINE [string range $CMDLINE [expr $CMDLINE_CURSOR] end] 565 | set CMDLINE_CURSOR 0 566 | } 567 | \u0017 { ;# ^w 568 | # delete the previous word 569 | set dstCursor [tcl_startOfPreviousWord $CMDLINE $CMDLINE_CURSOR] 570 | if {$dstCursor < 0} { 571 | set dstCursor 0 572 | } 573 | set CMDLINE [string replace $CMDLINE $dstCursor $CMDLINE_CURSOR] 574 | set CMDLINE_CURSOR $dstCursor 575 | } 576 | \u0008 - 577 | \u007f { ;# ^h && backspace ? 578 | if {$CMDLINE_CURSOR > 0} { 579 | incr CMDLINE_CURSOR -1 580 | set CMDLINE [string replace $CMDLINE \ 581 | $CMDLINE_CURSOR $CMDLINE_CURSOR] 582 | } 583 | } 584 | \u001b { ;# ESC - handle escape sequences 585 | handleEscapes 586 | } 587 | } 588 | # Rate limiter: 589 | set keybuffer "" 590 | } 591 | 592 | #proc TclReadLine::addCompletionHandler {completion_extension} { 593 | #variable COMPLETION_HANDLERS 594 | #set COMPLETION_HANDLERS [concat $completion_extension $COMPLETION_HANDLERS] 595 | #} 596 | 597 | #proc TclReadLine::delCompletionHandler {completion_extension} { 598 | #variable COMPLETION_HANDLERS 599 | #set COMPLETION_HANDLERS [lsearch -all -not -inline $COMPLETION_HANDLERS $completion_extension] 600 | #} 601 | 602 | #proc TclReadLine::getCompletionHandler {} { 603 | #variable COMPLETION_HANDLERS 604 | #return "$COMPLETION_HANDLERS" 605 | #} 606 | 607 | #proc TclReadLine::handleCompletion {} { 608 | #variable COMPLETION_HANDLERS 609 | #foreach handler $COMPLETION_HANDLERS { 610 | #if {[eval $handler] == 1} { 611 | #break 612 | #} 613 | #} 614 | #return 615 | #} 616 | 617 | proc TclReadLine::keyword {kw} { 618 | variable USER_KEYWORDS 619 | lappend USER_KEYWORDS $kw 620 | } 621 | 622 | proc TclReadLine::completePath {_files word wordstart lastchar} { 623 | variable CMDLINE 624 | upvar 1 $_files files ;# Our goal is to populate this list 625 | set globresult "" 626 | set head "" 627 | set pathonly 1 628 | 629 | if {$word == "../"} { ;# Exactly "../" 630 | catch [set globresult [glob -nocomplain -tails -directory ".." *]] 631 | set head ".." 632 | } elseif {$word == "./"} { ;# Exactly "./" 633 | catch [set globresult [glob -nocomplain *]] 634 | set head "." 635 | } elseif {$word == "~/"} { ;# #Exactly "~/" 636 | catch [set globresult [glob -nocomplain -tails -directory $::env(HOME) *]] 637 | set head $::env(HOME) 638 | } elseif {$word == "/"} { ;# Exactly "/" 639 | catch [set globresult [glob -nocomplain -tails -directory "/" *]] 640 | set head "/" 641 | } elseif {[string index $CMDLINE [expr $wordstart - 1]] == "/"} { 642 | if {[string index $CMDLINE [expr $wordstart - 2]] == "."} { 643 | if {[string index $CMDLINE [expr $wordstart - 3]] == "."} { ;# "../xxx" 644 | catch [set globresult [glob -nocomplain -tails -directory ".." $word*]] 645 | set head ".." 646 | } else { ;# "./xxx 647 | catch [set globresult [glob -nocomplain $word*]] 648 | set head "." 649 | } 650 | } elseif {[string index $CMDLINE [expr $wordstart - 2]] == "~"} { ;# "~/xxx 651 | catch [set globresult [glob -nocomplain -tails -directory $::env(HOME) $word*]] 652 | set head $::env(HOME) 653 | } else { ;# "/xxx 654 | catch [set globresult [glob -nocomplain -tails -directory "/" $word*]] 655 | set head "/" 656 | } 657 | } else { 658 | # The user hasn't typed any path notation so it's unclear if he wants to type file path, 659 | # but we collect candidates for file path completion anyway and 660 | # also collect candidates for other categories (like variables, commands, etc) 661 | set pathonly 0 662 | catch [set globresult [glob -nocomplain $word*]] 663 | set head "." 664 | } 665 | 666 | foreach f $globresult { 667 | if {[file isdirectory [file join $head $f]]} { 668 | # Append the file separator to directory names 669 | append f "/" 670 | } 671 | # Encode " " to "\ " like Unix shells handle file path containing spaces. 672 | set f [string map {{ } {\ }} $f] 673 | lappend files $f 674 | } 675 | 676 | return $pathonly 677 | } 678 | 679 | proc TclReadLine::handleCompletionBase {} { 680 | variable CMDLINE 681 | variable CMDLINE_CURSOR 682 | 683 | set vars "" 684 | set cmds "" 685 | set execs "" 686 | set files "" 687 | set extras "" 688 | 689 | # First find out what kind of word we need to complete: 690 | set wordend [expr $CMDLINE_CURSOR-1] 691 | set lastchar [string index $CMDLINE $wordend] 692 | set pathonly 0 693 | if {$lastchar == "/"} { 694 | set pathonly 1 695 | } elseif {$lastchar == {.} || $lastchar == {-} || $lastchar == {$}} { 696 | } elseif {[string is wordchar $lastchar] == 0} { 697 | return 698 | } 699 | set wordstart [string last " " $CMDLINE $wordend] 700 | while {[string index $CMDLINE [expr $wordstart-1]] == "\\"} { 701 | # Detect file path containing spaces. 702 | # You need to type "foo\ bar" to specify it is a file path 703 | # containing a space in it. 704 | incr wordstart -1 705 | set wordstart [string last " " $CMDLINE $wordstart] 706 | } 707 | incr wordstart 708 | set word [string range $CMDLINE $wordstart $wordend] 709 | 710 | if {[string trim $word " \{\}"] == ""} return 711 | 712 | if {[string is wordchar [string index $word 0]] == 0} { 713 | # The leftmost character of $word is not a word character yet. 714 | # Find the leftmost word boundary and reflect it to $word. 715 | set wordbreak [tcl_wordBreakAfter $word -1] 716 | if {$wordbreak > -1} { 717 | set word [string range $word $wordbreak end] 718 | incr wordstart $wordbreak 719 | } 720 | } 721 | 722 | set gp {*} 723 | if {[string length $word] > 1 && [string index $word end] == {$}} { 724 | set word [string replace $word end end {}] 725 | set gp {} 726 | } 727 | 728 | set specialchar [string index $CMDLINE [expr $wordstart - 1]] 729 | 730 | if {$pathonly == 1 || $specialchar == "/"} { 731 | # We assume the user wants to type file path... 732 | completePath files $word $wordstart $lastchar 733 | set pathonly 1 734 | } elseif {$specialchar == "\$"} { 735 | # We assume the user wants to type a variable name 736 | set x [string first "(" $word] ;# Check if it is an array key:proc 737 | if {$x != -1} { 738 | set v [string range $word 0 [expr {$x-1}]] 739 | incr x 740 | set word [string range $word $x end] 741 | incr wordstart $x 742 | if {[uplevel \#0 "array exists $v"]} { 743 | set vars [uplevel \#0 "array names $v $word*"] 744 | } 745 | } else { 746 | foreach x [uplevel \#0 {info vars}] { 747 | if {[string match "$word$gp" $x]} { 748 | lappend vars $x 749 | } 750 | } 751 | } 752 | } else { 753 | if {$wordstart == 0} { 754 | # Check executables: 755 | foreach dir [split $::env(PATH) :] { 756 | foreach f [glob -nocomplain -directory $dir -- $word*] { 757 | set exe [string trimleft [string range $f \ 758 | [string length $dir] end] "/"] 759 | 760 | if {[string match "$word$gp" $exe]} { 761 | lappend execs $exe 762 | } 763 | } 764 | } 765 | # Check commands: 766 | foreach x [info commands] { 767 | if {[string match "$word$gp" $x]} { 768 | lappend cmds $x 769 | } 770 | } 771 | } else { 772 | # Check file path: 773 | # $pathonly will be set to 1 when the user has typed file specific notations 774 | # like ./xxx, ../xxx, ~/xxx, etc... 775 | # And in that case, we ignore other categories because 776 | # it's weird to provide candidates for variables, commands, etc 777 | # when the user has explicitly typed path notations. 778 | set pathonly [completePath files $word $wordstart $lastchar] 779 | 780 | if {$pathonly == 0} { 781 | # Check commands anyway: 782 | foreach x [info commands] { 783 | if {[string match "$word$gp" $x]} { 784 | lappend cmds $x 785 | } 786 | } 787 | # Check ensemble commands 788 | set prevwordstart [tcl_startOfPreviousWord $CMDLINE $wordstart] 789 | set prevwordend [tcl_endOfWord $CMDLINE $prevwordstart] 790 | incr prevwordend -1 791 | set prevword [string range $CMDLINE $prevwordstart $prevwordend] 792 | if {[info command $prevword] != ""} { 793 | if {![catch {set ensemblecmds [namespace ensemble configure $prevword -map]}]} { 794 | dict for {x v} $ensemblecmds { 795 | if {[string match "$word$gp" $x]} { 796 | lappend cmds $x 797 | } 798 | } 799 | } 800 | } 801 | } 802 | 803 | } 804 | if {$wordstart != 0 && $pathonly == 0} { 805 | # Check variables anyway: 806 | set x [string first "(" $word] 807 | if {$x != -1} { 808 | set v [string range $word 0 [expr {$x-1}]] 809 | incr x 810 | set word [string range $word $x end] 811 | incr wordstart $x 812 | if {[uplevel \#0 "array exists $v"]} { 813 | set vars [uplevel \#0 "array names $v $word*"] 814 | } 815 | } else { 816 | foreach x [uplevel \#0 {info vars}] { 817 | if {[string match "$word$gp" $x]} { 818 | lappend vars $x 819 | } 820 | } 821 | } 822 | } 823 | } 824 | 825 | if {$pathonly == 0} { 826 | variable USER_KEYWORDS 827 | foreach kw $USER_KEYWORDS { 828 | if {[string match "$word$gp" $kw]} { 829 | lappend extras $kw 830 | } 831 | } 832 | } 833 | 834 | set maybe [lsort -unique [concat $vars $cmds $execs $files $extras]] 835 | if {[llength $maybe] > 1} { 836 | clearline 837 | set temp "" 838 | set temp2 "" 839 | foreach {match format} { 840 | vars "35" 841 | cmds "1;32" 842 | execs "32" 843 | files "0" 844 | extras "33" 845 | } { 846 | if {[llength [set $match]]} { 847 | append temp "[ESC]\[${format}m" 848 | foreach x [set $match] { 849 | append temp "[file tail $x] " 850 | append temp2 "$x " 851 | } 852 | append temp "[ESC]\[0m" 853 | } 854 | } 855 | print "\n$temp\n" 856 | # We provide the common longest prefix for the user to type less characters. 857 | set prefix [::tcl::prefix longest $temp2 $word] 858 | if {$prefix != ""} { 859 | set CMDLINE [string replace $CMDLINE $wordstart $wordend $prefix] 860 | set CMDLINE_CURSOR [expr {$wordstart+[string length $prefix]}] 861 | } 862 | } elseif {[llength $maybe]} { 863 | set match [lindex $maybe 0] 864 | if {$match != ""} { 865 | set CMDLINE \ 866 | [string replace $CMDLINE $wordstart $wordend $match] 867 | set CMDLINE_CURSOR \ 868 | [expr {$wordstart+[string length $match]}] 869 | } 870 | } 871 | } 872 | 873 | proc TclReadLine::handleHistory {x} { 874 | variable HISTORY_LEVEL 875 | variable HISTORY_SIZE 876 | variable CMDLINE 877 | variable CMDLINE_CURSOR 878 | variable CMDLINE_PARTIAL 879 | 880 | set hsMatch [string trim [processHistorySearch {} $x]] 881 | if {$hsMatch != {}} { 882 | set CMDLINE $hsMatch 883 | set CMDLINE_CURSOR [string length $hsMatch] 884 | return 885 | } 886 | 887 | set maxid [expr {[history nextid] - 1}] 888 | if {$maxid > 0} { 889 | # 890 | # Check for a top level command line and history event 891 | # Store this command line locally (i.e. don't use the history stack) 892 | # 893 | if {$HISTORY_LEVEL == 0} { 894 | set CMDLINE_PARTIAL $CMDLINE 895 | } 896 | incr HISTORY_LEVEL $x 897 | # 898 | # Note: HISTORY_LEVEL is used to offset into 899 | # the history events. It will be reset to zero 900 | # when a command is executed by tclline. 901 | # 902 | # Check the three bounds of 903 | # 1) HISTORY_LEVEL <= 0 - Restore the top level cmd line (not in history stack) 904 | # 2) HISTORY_LEVEL > HISTORY_SIZE 905 | # 3) HISTORY_LEVEL > maxid 906 | # 907 | if {$HISTORY_LEVEL <= 0} { 908 | set HISTORY_LEVEL 0 909 | if {[info exists CMDLINE_PARTIAL]} { 910 | set CMDLINE $CMDLINE_PARTIAL 911 | set CMDLINE_CURSOR [string length $CMDLINE] 912 | } 913 | return 914 | } elseif {$HISTORY_LEVEL > $maxid} { 915 | set HISTORY_LEVEL $maxid 916 | } elseif {$HISTORY_LEVEL > $HISTORY_SIZE} { 917 | set HISTORY_LEVEL $HISTORY_SIZE 918 | } 919 | set id [expr {($maxid + 1) - $HISTORY_LEVEL}] 920 | set cmd [expr {$id > $maxid ? "" : [history event $id]}] 921 | set CMDLINE $cmd 922 | set CMDLINE_CURSOR [string length $cmd] 923 | } 924 | } 925 | 926 | # History handling functions 927 | 928 | proc TclReadLine::getHistory {} { 929 | variable HISTORY_SIZE 930 | 931 | set l [list] 932 | set e [history nextid] 933 | set i [expr {$e - $HISTORY_SIZE}] 934 | if {$i <= 0} { 935 | set i 1 936 | } 937 | for { set i } {$i < $e} {incr i} { 938 | lappend l [history event $i] 939 | } 940 | return $l 941 | } 942 | 943 | proc TclReadLine::setHistory {hlist} { 944 | foreach event $hlist { 945 | history add $event 946 | } 947 | } 948 | 949 | # main() 950 | 951 | proc TclReadLine::rawInput {} { 952 | fconfigure stdin -buffering none -blocking 0 953 | fconfigure stdout -buffering none -translation crlf 954 | exec stty raw -echo 955 | } 956 | 957 | proc TclReadLine::lineInput {} { 958 | fconfigure stdin -buffering line -blocking 1 959 | fconfigure stdout -buffering line 960 | exec stty -raw echo 961 | } 962 | 963 | proc TclReadLine::doExit {{code 0}} { 964 | variable HISTFILE 965 | variable HISTORY_SIZE 966 | 967 | # Reset terminal: 968 | #print "[ESC]c[ESC]\[2J" nowait 969 | 970 | restore ;# restore "info' command - 971 | lineInput 972 | 973 | set hlist [getHistory] 974 | # 975 | # Get rid of the TclReadLine::doExit, shouldn't be more than one 976 | # 977 | set hlist [lsearch -all -not -inline $hlist "TclReadLine::doExit"] 978 | set hlistlen [llength $hlist] 979 | if {$hlistlen > 0} { 980 | set f [open $HISTFILE w] 981 | if {$hlistlen > $HISTORY_SIZE} { 982 | set hlist [lrange $hlist [expr {($hlistlen - $HISTORY_SIZE - 1)}] end] 983 | } 984 | foreach x $hlist { 985 | # Escape newlines: 986 | puts $f [string map { 987 | \n "\\n" 988 | "\\" "\\b" 989 | } $x] 990 | } 991 | close $f 992 | } 993 | 994 | exit $code 995 | } 996 | 997 | proc TclReadLine::restore {} { 998 | lineInput 999 | rename ::unknown TclReadLine::unknown 1000 | rename ::_unknown ::unknown 1001 | } 1002 | 1003 | proc TclReadLine::interact {} { 1004 | 1005 | rename ::unknown ::_unknown 1006 | rename TclReadLine::unknown ::unknown 1007 | 1008 | variable RCFILE 1009 | if {[info exists ::env(TCLLINERC)]} { 1010 | set RCFILE $::env(TCLLINERC) 1011 | } 1012 | if {[file exists $RCFILE]} { 1013 | source $RCFILE 1014 | } 1015 | 1016 | # Load history if available: 1017 | # variable HISTORY 1018 | variable HISTFILE 1019 | variable HISTORY_SIZE 1020 | history keep $HISTORY_SIZE 1021 | 1022 | if {[info exists ::env(TCLLINE_HISTORY)]} { 1023 | set HISTFILE $::env(TCLLINE_HISTORY) 1024 | } 1025 | if {[file exists $HISTFILE]} { 1026 | set f [open $HISTFILE r] 1027 | set hlist [list] 1028 | foreach x [split [read $f] "\n"] { 1029 | if {$x != ""} { 1030 | # Undo newline escapes: 1031 | lappend hlist [string map { 1032 | "\\n" \n 1033 | "\\\\" "\\" 1034 | "\\b" "\\" 1035 | } $x] 1036 | } 1037 | } 1038 | setHistory $hlist 1039 | unset hlist 1040 | close $f 1041 | } 1042 | 1043 | rawInput 1044 | 1045 | # This is to restore the environment on exit: 1046 | # Do not unalias this! 1047 | alias exit {TclReadLine::doExit} 1048 | 1049 | alias quit {TclReadLine::doExit} 1050 | alias help {TclReadLine::help} 1051 | 1052 | keyword quit 1053 | 1054 | variable ThisScript [info script] 1055 | 1056 | tclline ;# emit the first prompt 1057 | 1058 | fileevent stdin readable TclReadLine::tclline 1059 | variable forever 1060 | vwait TclReadLine::forever 1061 | 1062 | restore 1063 | } 1064 | 1065 | 1066 | proc TclReadLine::check_partial_keyseq {buffer} { 1067 | variable READLINE_LATENCY 1068 | upvar $buffer keybuffer 1069 | 1070 | # 1071 | # check for a partial esc sequence as tclline expects the whole sequence 1072 | # 1073 | if {[string index $keybuffer 0] == [ESC]} { 1074 | # 1075 | # Give extra time to read partial key sequences 1076 | # 1077 | set timer [expr {[clock clicks -milliseconds] + $READLINE_LATENCY}] 1078 | while {[clock clicks -milliseconds] < $timer } { 1079 | append keybuffer [read stdin] 1080 | } 1081 | } 1082 | } 1083 | 1084 | proc TclReadLine::addCmdToHistory {cmdline} { 1085 | if { $cmdline == {exit} || $cmdline == {quit} || $cmdline == {help} 1086 | || $cmdline == [history event 0] 1087 | || [string first {history} $cmdline] == 0 } { 1088 | return 1089 | } 1090 | # We encode all newlines. 1091 | # We keep all history records as single line strings, 1092 | # but we don't want to loose all those newlines. 1093 | set cmdline [string map {\n \\n} $cmdline] 1094 | history add $cmdline 1095 | } 1096 | 1097 | proc TclReadLine::hasIncompleteCommands {} { 1098 | variable MULTILINE_LIST 1099 | if {[llength $MULTILINE_LIST]} { 1100 | return 1 1101 | } else { 1102 | return 0 1103 | } 1104 | } 1105 | 1106 | proc TclReadLine::handleIncompleteCommands {} { 1107 | variable CMDLINE_CURSOR 1108 | variable CMDLINE 1109 | variable MULTILINE_LIST 1110 | 1111 | lappend MULTILINE_LIST $CMDLINE 1112 | set CMDLINE {} 1113 | set CMDLINE_CURSOR 0 1114 | print "\n" nowait 1115 | } 1116 | 1117 | proc TclReadLine::joinIncompleteCommands {} { 1118 | variable CMDLINE_CURSOR 1119 | variable CMDLINE 1120 | variable MULTILINE_LIST 1121 | 1122 | lappend MULTILINE_LIST $CMDLINE 1123 | set CMDLINE [join $MULTILINE_LIST "\n"] 1124 | set CMDLINE_CURSOR [string length $CMDLINE] 1125 | set MULTILINE_LIST [list] 1126 | } 1127 | 1128 | proc TclReadLine::tclline {} { 1129 | variable COLUMNS 1130 | variable CMDLINE_CURSOR 1131 | variable CMDLINE 1132 | 1133 | set char "" 1134 | set keybuffer [read stdin 1] 1135 | set COLUMNS [getColumns] 1136 | set pr "" 1137 | 1138 | check_partial_keyseq keybuffer 1139 | 1140 | while {$keybuffer != ""} { 1141 | if {[eof stdin]} return 1142 | set char [readbuf keybuffer] 1143 | if {$char == ""} { 1144 | # Sleep for a bit to reduce CPU overhead: 1145 | after 40 1146 | continue 1147 | } 1148 | 1149 | if {[string is print $char]} { 1150 | set x $CMDLINE_CURSOR 1151 | 1152 | set trailing [string range $CMDLINE $x end] 1153 | set CMDLINE [string replace $CMDLINE $x end] 1154 | append CMDLINE $char 1155 | append CMDLINE $trailing 1156 | incr CMDLINE_CURSOR 1157 | } elseif {$char == "\t"} { 1158 | #handleCompletion 1159 | handleCompletionBase 1160 | } elseif {$char == "\n" || $char == "\r"} { 1161 | if {[hasIncompleteCommands]} { 1162 | joinIncompleteCommands 1163 | } 1164 | 1165 | if {[string index $CMDLINE 0] == {?}} { 1166 | # Search command with given words from the command history 1167 | set searchWord [string range $CMDLINE 1 end] 1168 | if {$searchWord == {}} { 1169 | setHistorySearchState SWITCH_OFF 1170 | processHistorySearch {} 1171 | set CMDLINE {} 1172 | set CMDLINE_CURSOR 0 1173 | } else { 1174 | setHistorySearchState SWITCH_ON 1175 | set CMDLINE [processHistorySearch $searchWord] 1176 | set CMDLINE_CURSOR [string length $CMDLINE] 1177 | } 1178 | } elseif {$CMDLINE == {}} { 1179 | setHistorySearchState SWITCH_OFF 1180 | processHistorySearch {} 1181 | print "\n" nowait 1182 | } elseif {[info complete $CMDLINE]} { 1183 | lineInput 1184 | print "\n" nowait 1185 | uplevel \#0 { 1186 | 1187 | # Handle aliases: 1188 | set cmdline [string trimleft $TclReadLine::CMDLINE] 1189 | # 1190 | # Add the cmd line to history before doing any substitutions 1191 | # 1192 | TclReadLine::addCmdToHistory $cmdline 1193 | set cmd [string trim [regexp -inline {^\s*[^\s]+} $cmdline]] 1194 | if {[info exists TclReadLine::ALIASES($cmd)]} { 1195 | regsub -- "(?q)$cmd" $cmdline $TclReadLine::ALIASES($cmd) cmdline 1196 | } 1197 | 1198 | # Perform glob substitutions: 1199 | set cmdline [string map { 1200 | "\\*" \0 1201 | "\\~" \1 1202 | } $cmdline] 1203 | # 1204 | # Prevent glob substitution of *,~ for tcl commands 1205 | # 1206 | if {[info commands $cmd] != ""} { 1207 | set cmdline [string map { 1208 | "\*" \0 1209 | "\~" \1 1210 | } $cmdline] 1211 | } 1212 | while {[regexp -indices \ 1213 | {([\w/\.]*(?:~|\*)[\w/\.]*)+} $cmdline x] 1214 | } { 1215 | foreach {i n} $x break 1216 | set s [string range $cmdline $i $n] 1217 | set x [glob -nocomplain -- $s] 1218 | 1219 | # If glob can't find anything then don't do 1220 | # glob substitution, pass * or ~ as literals: 1221 | if {$x == ""} { 1222 | set x [string map { 1223 | "*" \0 1224 | "~" \1 1225 | } $s] 1226 | } 1227 | set cmdline [string replace $cmdline $i $n $x] 1228 | } 1229 | set cmdline [string map { 1230 | \0 "*" 1231 | \1 "~" 1232 | } $cmdline] 1233 | 1234 | rename ::info ::_info 1235 | rename TclReadLine::localInfo ::info 1236 | 1237 | # Reset HISTORY_LEVEL before next command 1238 | set TclReadLine::HISTORY_LEVEL 0 1239 | if {[info exists TclReadLine::CMDLINE_PARTIAL]} { 1240 | unset TclReadLine::CMDLINE_PARTIAL 1241 | } 1242 | 1243 | # Decode newlines. See addCmdToHistory proc for detail. 1244 | set cmdline [string map {\\\\n \n \\n \n} $cmdline] 1245 | 1246 | if {[string index $cmdline 0] == {$}} { 1247 | set varname [string range $cmdline 1 end] 1248 | if {[info exists $varname]} { 1249 | # Print a variable value when just typing that variable. 1250 | # e.g.) It makes typing `$foo` equivalent to typing `puts $foo` 1251 | if {[array exists $varname]} { 1252 | set cmdline "array get $varname" 1253 | } else { 1254 | set cmdline [join "puts $cmdline"] 1255 | } 1256 | } 1257 | } 1258 | 1259 | # Run the command: 1260 | set code [catch $cmdline res] 1261 | rename ::info TclReadLine::localInfo 1262 | rename ::_info ::info 1263 | if {$code == 1} { 1264 | TclReadLine::print "[TclReadLine::ESC]\[31m$::errorInfo[TclReadLine::ESC]\[0m\n" 1265 | } else { 1266 | TclReadLine::print "$res\n" 1267 | } 1268 | 1269 | TclReadLine::setHistorySearchState SWITCH_OFF 1270 | 1271 | set TclReadLine::CMDLINE "" 1272 | set TclReadLine::CMDLINE_CURSOR 0 1273 | set TclReadLine::CMDLINE_LINES {0 0} 1274 | } ;# end uplevel 1275 | rawInput 1276 | } else { 1277 | handleIncompleteCommands 1278 | } 1279 | } else { 1280 | handleControls 1281 | } 1282 | } 1283 | if {[hasIncompleteCommands]} { 1284 | set pr {~ } 1285 | } 1286 | prompt $CMDLINE $pr 1287 | } 1288 | 1289 | # start immediately if invoked as a script: 1290 | if {!$::tcl_interactive && [info script] eq $::argv0} { 1291 | TclReadLine::interact 1292 | } 1293 | 1294 | # Put the following in your .tclshrc 1295 | #if {$::tcl_interactive} { 1296 | ##package require TclReadLine 1297 | #TclReadLine::interact 1298 | #} 1299 | 1300 | -------------------------------------------------------------------------------- /TclReadLine/help.txt: -------------------------------------------------------------------------------- 1 | ##### Key Bindings 2 | - CONTROL + a / HOME 3 | - Move the cursor to the start of the line 4 | - CONTROL + b / LEFT 5 | - Move the cursor to the previous character 6 | - CONTROL + d 7 | - Terminate the shell. Same as executing `exit` command. 8 | - CONTROL + e / END 9 | - Move the cursor to the end of the line 10 | - CONTROL + f / RIGHT 11 | - Move the cursor to the next character 12 | - CONTROL + g 13 | - Clear the entire line 14 | - CONTROL + k 15 | - Clear everything from the cursor to the end of the line 16 | - And the cleared text will be YANKED (remembered) 17 | - CONTROL + y 18 | - Paste the text previously yanked 19 | - CONTROL + n / DOWN 20 | - Retrieve an older command line from the command history 21 | - CONTROL + p / UP 22 | - Retrieve a newer command line from the command history 23 | - CONTROL + u 24 | - Clear everything from the start of the line to the cursor 25 | - And the cleared text will be YANKED (remembered) 26 | - CONTROL + w 27 | - Delete the previous word 28 | - CONTROL + h / Backspace 29 | - Delete the previous character 30 | - TAB 31 | - Keyword completion for commands, variable, file path, etc. 32 | - ALT + 0 33 | - Move the cursor to the start of the line. Same as CONTROL + a 34 | - ALT + b 35 | - Move to the start of the word 36 | - ALT + d 37 | - Delete the next word 38 | - ALT + e 39 | - Move to the end of the word 40 | - ALT + f / ALT + w 41 | - Move to the start of the next word 42 | - ALT + / 43 | - Clear the entire line and insert `?` at the start of the line. 44 | - This will quickly let you search commands with glob patterns through the command history 45 | - Read *History Search Mode* in the documentation 46 | - ALT + k 47 | - Retrieve an older command line from the command history 48 | - Same as CONTROL + p or UP 49 | - ALT + j 50 | - Retrieve a newer command line from the command history 51 | - Same as CONTROL + n or DOWN 52 | - ALT + h 53 | - Move the cursor to the previous character 54 | - Same as CONTROL + b or LEFT 55 | - ALT + l 56 | - Move the cursor to the next character 57 | - Same as CONTROL + f or RIGHT 58 | - ALT + x / DELETE 59 | - Delete the next character 60 | - Shift + LEFT 61 | - Move the cursor to the previous blank 62 | - Shift + RIGHT 63 | - Move the cursor to the next blank 64 | 65 | ##### Commands 66 | - exit 67 | - Exit Tclsh-Wrapper 68 | - quit 69 | - Same as exit 70 | - help 71 | - Print out help messages 72 | - clear 73 | - Clear the screen 74 | - keyword 75 | - Add an arbitrary user keyword which will be expanded on keyword completion 76 | - alias 77 | - Define an alias for an arbitrary command 78 | - unalias 79 | - Undefine an aliase previously defined 80 | -------------------------------------------------------------------------------- /TclReadLine/pkgIndex.tcl: -------------------------------------------------------------------------------- 1 | # Tcl package index file, version 1.1 2 | # This file is generated by the "pkg_mkIndex" command 3 | # and sourced either when an application starts up or 4 | # by a "package unknown" script. It invokes the 5 | # "package ifneeded" command to set up package-related 6 | # information so that packages will be loaded automatically 7 | # in response to "package require" commands. When this 8 | # script is sourced, the variable $dir must contain the 9 | # full path name of this file's directory. 10 | 11 | package ifneeded TclReadLine 1.4 [list source [file join $dir TclReadLine.tcl]] 12 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018- Suewon Bahng, https://github.com/suewonjp 2 | 3 | Last Updated (2018-02-23) 4 | 5 | ## Version 1.4 (2018-02-23) 6 | - Bug fix : issue when pasting newline attached command from the system clipboard 7 | - Bug fix : fatal exception on some keyword autocompletion situations 8 | - Bug fix : issue on keyword completion for file path containing spaces 9 | - Read *Support for file path containing spaces* section in README for more detail 10 | - Feature : support for multiline commands 11 | - Feature : keyword completion for "ensemble" commands 12 | - Feature : 'help' command to print out available key bindings & other help messages 13 | - Feature : math operations and functions available at the command line 14 | - https://github.com/suewonjp/tclsh-wrapper/issues/8 15 | - Feature : new command 'quit' which is equivalent to `exit` 16 | - Feature : simple print for variables 17 | - Typing "$foo" is equivalent to typing "puts $foo" 18 | 19 | ## Version 1.3 (2018-01-26) 20 | - Bug fix : keyword autocompletion failed when .tcllinerc file didn't exist 21 | - Added sample of .tcllinerc file 22 | 23 | ## Version 1.2 (2018-01-12) 24 | - First Release 25 | 26 | -------------------------------------------------------------------------------- /tcl.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$1" = "w" ]; then 4 | TCLLINE_HISTORY=~/.tclline_history_w exec wish ~/lib/tcl/tclsh-wrapper/TclReadLine/TclReadLine.tcl 5 | else 6 | TCLLINE_HISTORY=~/.tclline_history exec tclsh ~/lib/tcl/tclsh-wrapper/TclReadLine/TclReadLine.tcl 7 | fi 8 | 9 | --------------------------------------------------------------------------------