Index: tcl053.js ================================================================== --- tcl053.js +++ tcl053.js @@ -9,27 +9,30 @@ * Richard Suchenwirth 2007, 2013: cleanup, additions * vim: syntax=javascript autoindent softtabwidth=4 */ // 'use strict'; // breaks some tests, like expr 0376, for loop var _step = 0; // set to 1 for debugging -var fs = require('fs'); -var puts = console.log; +if(process.env["DEBUG"] == 1) _step = 1; +var fs = require('fs'); +var puts = console.log; // saves a lot of typing... ;^) function TclInterp () { this.patchlevel = "0.5.3"; this.callframe = [{}]; this.level = 0; this.levelcall = []; this.commands = {}; this.procs = []; this.script = ""; + this.getsing = 0; this.OK = 0; this.RET = 1; this.BRK = 2; this.CNT = 3; this.getVar = function(name) { - var nm = name.toString(); + var nm = name.toString(); + // no arrays supported yet, but a read-only exception for ::env() if (nm.match("^::env[(]")) nm = nm.substr(2); if (nm.match("^env[(]")) { var key = nm.substr(4,nm.length-5); var val = process.env[key]; } else if (nm.match("^::")) { @@ -39,23 +42,24 @@ } if (val == null) throw 'can\'t read "'+name+'": no such variable'; return val; } this.setVar = function(name, val) { - var nm = name.toString(); - if (nm.match("^::")) { - this.callframe[0][nm.substr(2)] = val; - } else {this.callframe[this.level][name] = val;} - return val; + var nm = name.toString(); + if (val != null && val.toString().match(/\\/)) + val = eval("'"+val.toString()+"'"); + if (nm.match("^::")) { + this.callframe[0][nm.substr(2)] = val; + } else {this.callframe[this.level][name] = val;} + return val; } this.setVar("argc", process.argv.length-2); this.setVar("argv0", process.argv[1]); this.setVar("argv", process.argv.slice(2)); this.setVar("errorInfo", ""); this.incrLevel = function() { - //puts("going down from ("+this.level+"): "+this.levelcall); this.callframe[++this.level] = {}; return this.level; } this.decrLevel = function() { this.callframe[this.level] = null; @@ -104,10 +108,11 @@ if(msg.length >= 125) msg += "..."; var msg = e+'\n while executing\n"'+msg+'"'; for(var i = this.level; i > 0; i--) msg += '\n invoked from within\n"'+this.levelcall[i]+'"' this.setVar("::errorInfo", msg); + if(_step) puts("e: "+e); throw e; } } this.eval2 = function(code) { this.code = this.OK; @@ -170,10 +175,15 @@ } if (args.length > 0) result = this.call(args); return this.objectify(result); } //---------------------------------- Commands in alphabetical order + /*this.registerCommand("after", function (interp, args) { + this.arity(args, 3); + var code = args[2].toString(); + setTimeout(args[1], function(code) {interp.eval(code)}); + });*/ this.registerCommand("append", function (interp, args) { this.arity(args, 2, Infinity); var vname = args[1].toString(); try {var str = interp.getVar(vname);} catch(e) {var str = "";} for (var i = 2; i < args.length; i++) str += args[i].toString(); @@ -204,12 +214,14 @@ interp.code = interp.CNT; return; }); this.registerSubCommand("clock", "format", function (interp, args) { var now = new Date(); - now.setTime(args[1]); - return now.toString(); + now.setTime(args[1]*1000); + var ts = now.toString().split(" "); + var tz = ts[6].toString().replace("(","").replace(")",""); + return ts[0]+" "+ts[1]+" "+ts[2]+" "+ts[4]+" "+tz+" "+ts[3]; }); this.registerSubCommand("clock", "milliseconds", function (interp, args) { var t = new Date(); return t.valueOf(); }); @@ -358,30 +370,71 @@ this.registerCommand("expr", function (interp, args) { var expression = args.slice(1).join(" "); return interp.expr(interp, expression); }); this.expr = function (interp, expression) { // also used in for, if, while + var mx; try { - var mx = expression.match(/(\[.*\])/g); + mx = expression.match(/(\[.*\])/g); for (var i in mx) puts("have to deal with "+mx[i].toString()); } catch(e) {puts(i+". exception: "+e);} mx = expression.match(/(\$[A-Za-z0-9_:]+)/g); - for (i in mx) { + for (var i in mx) { var val = interp.getVar(mx[i].slice(1)).toString(); if(isNaN(val) || !isFinite(val)) val = '"'+val+'"'; eval("var "+mx[i]+' = '+val); } var res = eval(expression); if(res == false) res = 0; else if(res == true) res = 1; return res; }; + this.registerSubCommand("file", "atime", function (interp, args) { + this.arity(args, 2); + var stat = fs.statSync(args[1].toString()); + return stat.atime.getTime()/1000; + }) this.registerSubCommand("file", "dirname", function (interp, args) { this.arity(args, 2); - var path = args[1].toString().split("/"); - path.pop(); - return path.join("/"); + return interp.dirname(args[1].toString()); + }); + this.dirname = function(p) { // also used in [glob] + //require("path"); //not working :( + //return path.dirname(p.toString()); + if(p == ".") p = process.cwd(); + p = p.split("/"); + p.pop(); + if(p == "") return("/"); + return p.join("/"); + }; + this.registerSubCommand("file", "exists", function (interp, args) { + this.arity(args, 2); + var file = args[1].toString(); + try {var fd = fs.openSync(file,"r");} catch(e) {return 0;} + fs.closeSync(fd); + return 1; + }); + this.registerSubCommand("file", "extension", function (interp, args) { + this.arity(args, 2); + var fn = args[1].toString(); + var res = fn.split(".").pop(); + res = (res == fn)? "" : "."+res; + return res; + }); + this.registerSubCommand("file", "mtime", function (interp, args) { + this.arity(args, 2); + var stat = fs.statSync(args[1].toString()); + return stat.mtime.getTime()/1000; + }) + this.registerSubCommand("file", "size", function (interp, args) { + this.arity(args, 2); + var stat = fs.statSync(args[1].toString()); + return stat.size; + }) + this.registerSubCommand("file", "tail", function (interp, args) { + this.arity(args, 2); + return args[1].toString().split("/").pop(); }); this.registerCommand("for", function (interp, args) { this.arity(args, 5); interp.eval(args[1].toString()); if(interp.code != interp.OK) return; @@ -432,34 +485,58 @@ return x.toString(16); } else if(fmt=="%X") { var x = new Number(val); return x.toString(16).toUpperCase(); } - else throw "unknown format"; - - }); - /* - this.registerCommand("gets", function (interp, args) { - this.arity(args, 2, 3); - this.tmp = null; - rl.on('line', itp.gets); - while(true) { - if(this.tmp != null) break; - }; - rl.on('line', this.getsevalputs); - var reply = this.tmp; - // = prompt(args[1],""); - //process.stdin.resume(); - //process.stdin.on('data', function(str) { - //reply = str; - // }); - if(args[2] != null) { - interp.setVar(args[2],interp.objectify(reply)); - return reply.length; - } else return reply; - }); - */ + else throw "unknown format"; + }); + this.registerCommand("gets0", function (interp, args) { + this.arity(args, 2, 3); + interp.getsing = 1; + //interp.buf = ""; + //while(interp.buf == "") { + //interp.timeout = setTimeout(function(){}, 10000); + // if(interp.getsing==0) break; + //} + return; // result will be in interp.buf when done + }); + this.gets = function(char) { + try { + if(char.match(/foo[\r\n]/)) { + this.getsing = 0; + puts("received: "+this.buf); + } else { + puts("<"+char+">"+this.getsing); + this.buf += char; + } + } catch(e) {puts(e)}; + } + this.registerCommand("glob", function (interp, args) { + this.arity(args, 2, Infinity); + args.shift(); + var res = []; + var prefix = ""; + var dir = "."; + for (var arg in args) { + var path = args[arg].toString(); + if(path.match("[/]")) { + var dir = interp.dirname(path); + var prefix = dir+"/"; + } + var pattern = path.split("/").pop().replace(/[*]/g,".*"); + var files = fs.readdirSync(dir); + for (var i in files) { + if(files[i].match("^[.]")) continue; + if(files[i] == pattern) {res.push(files[i]);} + if(files[i].match("^"+pattern+"$")) { + var file = (dir == ".")? files[i] : dir+"/"+files[i]; + res.push(file.replace(/\/\//,"/")); + } + } + } + return res; + }); this.registerCommand("if", function (interp, args) { this.arity(args, 3, Infinity); var cond = args[1].toString(); var test = interp.objectify(interp.expr(interp, cond)); if (test.toBoolean()) return interp.eval(args[2].toString()); @@ -537,11 +614,11 @@ this.registerSubCommand("info", "script", function (interp, args) { return interp.script; }); this.registerSubCommand("info", "vars", function (interp, args) { var res = []; - for(i in interp.callframe[interp.level]) { + for(var i in interp.callframe[interp.level]) { try { if(interp.getVar(i) != null) {res.push(i);} } catch(e) {}; } return res; @@ -599,13 +676,13 @@ this.arity(args, 2); return args[1].toList().length; }); this.registerCommand("lrange", function (interp, args) { this.arity(args, 4); - var list = interp.objectify(args[1]); + var list = interp.objectify(args[1]); var start = list.listIndex(args[2]); - var end = list.listIndex(args[3])+1; + var end = list.listIndex(args[3])+1; try { return list.content.slice(start, end); } catch (e) {return [];} }); this.registerCommand("lreverse", function (interp, args) { @@ -693,15 +770,16 @@ }); this.registerCommand("source", function (interp, args) { this.arity(args, 2); interp.script = args[1].toString(); try { - var data = fs.readFileSync(interp.script).toString(); + var data = fs.readFileSync(interp.script,{encoding: 'utf8'}).toString(); } catch(e) { + puts("e: "+e); throw 'couldn\' read file "'+interp.script +'": no such file or directory';} - var res = interp.eval(data); + var res = interp.eval(data); interp.script = ""; return res; }); this.registerCommand("split", function (interp, args) { this.arity(args, 2, 3); @@ -886,11 +964,11 @@ Tcl.isOctal = new RegExp("^[+\\-]?0[0-7]*$"); Tcl.isHexSeq = new RegExp("[0-9a-fA-F]*"); Tcl.isOctalSeq = new RegExp("[0-7]*"); Tcl.isList = new RegExp("[\\{\\} ]"); Tcl.isNested = new RegExp("^\\{.*\\}$"); -Tcl.getVar = new RegExp("^[a-zA-Z0-9_]+", "g"); +Tcl.getVar = new RegExp("^[:a-zA-Z0-9_]+", "g"); Tcl.Proc = function (interp, args) { var priv = this.privdata; interp.incrLevel(); var arglist = priv[0].toList(); @@ -922,11 +1000,11 @@ break; } interp.setVar(name, interp.objectify(args[i])); } if (name == "args" && i+1 < arglist.length) - throw "'args' should be the last argument"; + throw "'args' should be the last argument"; try { var res = interp.eval(body); interp.code = interp.OK; interp.decrLevel(); return res; @@ -942,12 +1020,11 @@ args[0] = main; var ens = this.ensemble; if (ens == null) { throw "Not an ensemble command: "+main; } else if (ens[sub] == null) { - - var matches = 0, lastmatch = ""; + var matches = 0, lastmatch = ""; for (var i in ens) { // maybe unambiguous prefix? if (i.match("^"+sub) != null) { matches += 1; lastmatch = i; } @@ -1107,13 +1184,13 @@ this.func = func; this.privdata = privdata; this.ensemble = arguments[2]; this.call = function(interp, args) { - var r = (this.func)(interp, args); - r = interp.objectify(r); - return r; + var res = (this.func)(interp, args); + res = interp.objectify(res); + return res; } this.arity = function (args, min, max) { if(max == undefined) max = min; if (args.length < min || args.length > max) { throw min + ".."+max + " words expected, got "+args.length; @@ -1139,30 +1216,30 @@ this.cur = this.text.charAt(0); this.getText = function () { return this.text.substring(this.start,this.end+1); } this.parseString = function () { - var newword = (this.type==this.SEP || + var newword = (this.type == this.SEP || this.type == this.EOL || this.type == this.STR); if (newword && this.cur == "{") return this.parseBrace(); else if (newword && this.cur == '"') { this.insidequote = true; this.feedchar(); } this.start = this.index; while (true) { if (this.len == 0) { - this.end = this.index-1; + this.end = this.index-1; this.type = this.ESC; return this.OK; } /*if (this.cur == "\\") { // works not :( if (this.len >= 2) this.feedSequence(); } else*/ if ("$[ \t\n\r;".indexOf(this.cur)>=0) { if ("$[".indexOf(this.cur)>=0 || !this.insidequote) { - this.end = this.index-1; + this.end = this.index-1; this.type = this.ESC; return this.OK; } } else if (this.cur == '"' && this.insidequote) { @@ -1175,11 +1252,11 @@ this.feedchar(); } return this.OK; } this.parseList = function () { - var level = 0; + var level = 0; this.start = this.index; while (true) { if (this.len == 0) { this.end = this.index; this.type = this.EOL; @@ -1301,12 +1378,11 @@ this.parseComment(); continue; } return this.parseString(); } - puts("unreachable?"); - return this.OK; // unreached + //return this.OK; // unreached } this.feedSequence = function () { //return; if (this.cur != "\\") throw "Invalid escape sequence"; var cur = this.steal(1); @@ -1362,14 +1438,14 @@ this.feedcharstart = function () { this.feedchar(); this.start = this.index; } this.setPos = function (index) { - var d = index-this.index; + var d = index-this.index; this.index = index; - this.len -= d; - this.cur = this.text.charAt(this.index); + this.len -= d; + this.cur = this.text.charAt(this.index); } this.feedchar = function () { this.index++; this.len--; if (this.len < 0) @@ -1382,24 +1458,21 @@ var res; process.argv.slice(2).forEach(function(cmd,index,array) { itp.eval(cmd); }); var readline = require('readline'); -var rl = readline.createInterface(process.stdin, process.stdout); +var rl = readline.createInterface(process.stdin, process.stdout); rl.setPrompt('% '); rl.prompt(); -itp.getsevalputs = function(line) { +itp.gets = function(line) { + if (itp.getsing == 0) { try { res = itp.eval(line.trim()); } catch(e) {res = e;} - if(res != null && res.toString() != "" && res.toString().length) - puts(res.toString()); - rl.prompt(); + if (itp.getsing == 0) { + if(res != null && res.toString().length) + puts(res.toString()); + rl.prompt(); + } + } else {itp.buf = line; itp.getsing = 0; rl.prompt();} }; -itp.gets = function(line) { - puts("received "+line); - this.tmp = line; -} -rl.on('line', itp.getsevalputs -).on('close',function() { - process.exit(0); - }); +rl.on('line', itp.gets).on('close',function() {process.exit(0);}); Index: test_tcljs.tcl ================================================================== --- test_tcljs.tcl +++ test_tcljs.tcl @@ -6,18 +6,19 @@ set vars [info vars] ;# for later cleanup set version 0.5.3 set total 0 set passed 0 set fail 0 -puts "------------------------ [info script] patchlevel: [info patchlevel]" +puts "----- [info script] of [clock format [file mtime [info script]]], patchlevel: [info patchlevel]" proc e.g. {cmd -> expected} { + #puts $cmd incr ::total incr ::fail ;# to also count exceptions set res [uplevel 1 $cmd] if ![string equal $res $expected] { - #if {$res != $expected} {} #should work, but wouldn't + #if {$res != $expected} {} puts "**** $cmd -> $res, expected: $expected" } else {incr ::passed; incr ::fail -1} } # e.g. {exec echo hello} -> hello ;# needs blocking exec @@ -26,19 +27,20 @@ e.g. {set x foo} -> foo e.g. {append x bar} -> foobar proc sum args {expr [join $args +]} e.g. {sum 1 2 3} -> 6 -#native sum2 {function (interp, args) {return eval(args.join("+"));}} -#e.g. {sum2 2 3 4} -> 9 -e.g. {catch foo msg} -> 1 -e.g. {set msg} -> {invalid command name "foo"} +e.g. {catch foo msg} -> 1 +e.g. {set msg} -> {invalid command name "foo"} e.g. {catch {expr 7*6}} -> 0 e.g. {catch {expr 7*6} msg; set msg} -> 42 +e.g. {clock format 0} -> {Thu Jan 01 01:00:00 CET 1970} + e.g. {concat {a b} {c d}} -> {a b c d} +e.g. {concat $::version} -> $version e.g. {set d [dict create a 1 b 2 c 3]} -> {a 1 b 2 c 3} e.g. {dict exists $d c} -> 1 e.g. {dict exists $d x} -> 0 e.g. {dict get $d b} -> 2 @@ -49,20 +51,22 @@ e.g. {dict unset d x} -> {a 1 c 3} e.g. {dict unset d nix} -> {a 1 c 3} e.g. {dict set dx a 1} -> {a 1} ;# create new dict if not exists e.g. {set home [file dirname [pwd]]; list} -> {} -e.g. {string equal [set env(HOME)] $home} -> 1 +e.g. {string equal [set env(HOME)] $home} -> 1 +# e.g. {string equal $::env(HOME) $home} -> 1 e.g. {string equal [set ::env(HOME)] $home} -> 1 +e.g. {file dirname /foo/bar/grill} -> /foo/bar +e.g. {file tail /foo/bar/grill} -> grill e.g. {expr 6*7} -> 42 e.g. {expr {6 * 7 + 1}} -> 43 e.g. {set x 43} -> 43 e.g. {expr {$x-1}} -> 42 e.g. {expr $x-1} -> 42 if ![info exists auto_path] { ;#these tests are not for a real tclsh - e.g. {clock format 0} -> {Thu Jan 01 1970 01:00:00 GMT+0100 (CET)} e.g. {set i [expr 1/0]} -> Infinity e.g. {expr $i==$i+42} -> 1 e.g. {set n [expr sqrt(-1)]} -> NaN e.g. {expr $n == $n} -> 0 e.g. {expr $n==$n} -> 0 @@ -92,10 +96,16 @@ e.g. {expr {$x != $y}} -> 1 e.g. {expr 43 % 5} -> 3 e.g. {set x -44; expr {-$x}} -> 44 e.g. {expr 1<<3} -> 8 +e.g. {file dirname foo/bar/grill} -> foo/bar +e.g. {file dirname /foo/bar/grill} -> /foo/bar +e.g. {file extension foo.txt} -> .txt +e.g. {file extension Makefile} -> "" +e.g. {file tail foo/bar/grill} -> grill + set forres "" e.g. {for {set i 0} {$i < 5} {incr i} {append forres $i}; set forres} -> 01234 e.g. {foreach i {a b c d e} {append foreachres $i}; set foreachres} -> abcde e.g. {format %x 255} -> ff @@ -104,18 +114,18 @@ e.g. {set x 41} -> 41 e.g. {incr x} -> 42 e.g. {incr x 2} -> 44 e.g. {incr x -3} -> 41 -e.g. {info args e.g.} -> {cmd -> expected} +e.g. {info args e.g.} -> {cmd -> expected} e.g. {unset -nocomplain foo} -> {} -e.g. {info exists foo} -> 0 -e.g. {set foo 42} -> 42 -e.g. {info exists foo} -> 1 -e.g. {info level} -> 0 ;# e.g. runs the command one level up +e.g. {info exists foo} -> 0 +e.g. {set foo 42} -> 42 +e.g. {info exists foo} -> 1 +e.g. {info level} -> 0 ;# e.g. runs the command one level up e.g. {proc f x {set y 0; info vars}} -> "" -e.g. {f 41} -> {x y} +e.g. {f 41} -> {x y} set tmp [f 40]; e.g. {lappend tmp z} -> {x y z} e.g. {info args f} -> x e.g. {info body f} -> {set y 0; info vars} e.g. {info bod f} -> {set y 0; info vars} @@ -140,10 +150,11 @@ e.g. {proc f args {expr [join $args +]}} -> "" e.g. {f 1} -> 1 e.g. {f 1 2} -> 3 e.g. {f 1 2 3} -> 6 +e.g. {proc f {arg b} {expr $arg*$b}; f 6 7} -> 42 ;# should work with 'args' e.g. {regexp {X[ABC]Y} XAY} -> 1 e.g. {regexp {X[ABC]Y} XDY} -> 0 e.g. {regsub {[A-C]+} uBAAD x} -> uxD @@ -152,30 +163,32 @@ e.g. {split "a b c d "} -> {a b {} c d {}} e.g. {split usr/local/bin /} -> {usr local bin} e.g. {split /usr/local/bin /} -> {{} usr local bin} e.g. {split abc ""} -> {a b c} -e.g. {string compare a b} -> -1 -e.g. {string compare b a} -> 1 -e.g. {string compare b b} -> 0 -e.g. {string equal foo foo} -> 1 -e.g. {string equal foo bar} -> 0 -e.g. {string index abcde 2} -> c -e.g. {string length ""} -> 0 -e.g. {string length foo} -> 3 -e.g. {string range hello 1 3} -> ell -e.g. {string tolower Tcl} -> tcl -e.g. {string toupper Tcl} -> TCL -e.g. {string trim " foo "} -> foo - -e.g. {set x a.\x62.c} -> a.b.c ;# severe malfunction, breaks test suite operation :( +e.g. {string compare a b} -> -1 +e.g. {string compare b a} -> 1 +e.g. {string compare b b} -> 0 +e.g. {string equal foo foo} -> 1 +e.g. {string equal foo bar} -> 0 +e.g. {string index abcde 2} -> c +e.g. {string length ""} -> 0 +e.g. {string length foo} -> 3 +e.g. {string range hello 1 3} -> ell +e.g. {string range hello 1 end} -> ello +e.g. {string tolower Tcl} -> tcl +e.g. {string toupper Tcl} -> TCL +e.g. {string trim " foo "} -> foo + +e.g. {set x a.\x62.c} -> a.b.c +e.g. {set e \u20ac} -> "€" ;# breaks in node v0.6.19, works in v0.10.22 + puts "total $total tests, passed $passed, failed $fail" #----------- clean up variables used in tests foreach var [info vars] { set pos [lsearch $vars $var] ;# expr can't substitute commands yet - set neq [string compare $var vars] if {$var != "vars" && $pos < 0} {unset $var} } -unset vars var pos neq +unset vars var pos puts "vars now: [info vars]" puts "[llength [info commands]] commands implemented"