I'm launching multiple tclsh from inside a TCL script to emulate multi threading. However the calls all fail.
I've simplified the problem down to a test where a TCL proc launches a tclsh.
proc launch_tcl {} {
set cmd "tclsh script.tcl"
set pid [ eval $cmd & ]
}
This produces : invalid command name "tclsh"
I can give following lines on the TCL prompt and they work fine.
set cmd "tclsh script.tcl"
set pid [ eval $cmd & ]
I have tried tclsh with full path to the binary as well with same failure.
Why does the same commands fail inside the proc?
Thanks,
Gert
While Tcl syntax looks more related to shell syntax like bash or tcsh tcl is actually more closely related to Perl or PHP or Ruby. Tcl only interprets tcl code thus just typing the name of another executable does not launch that executable*.
Just like Perl or Ruby (or indeed C and C++) tcl does indeed have mechanisms to launch executables. For that you need the exec command:
exec tclsh script.tcl
Warning on how exec works:
Unlike other languages where the command to spawn external binaries accept a string, tcl's exec is more closely related to C or javascript in that it accepts structured data. If you try to do this:
exec "tclsh script.tcl"
You'll get an error complaining "tclsh script.tcl" does not exist. It will look for an executable called "tclsh script.tcl" (because unix, from the very beginning, allows program names to contain spaces). Instead you need to pass the program name and each individual argument separately. Similarly if you tried:
exec tclsh "arg1 arg2"
The exec command will execute tclsh and pass the string "arg1 arg2" as the first argument.
See the documentation of exec for more info: https://www.tcl.tk/man/tcl/TclCmd/exec.htm
What to do if you have a command in a string?
Don't store commands in strings. There's no real safe way to separate arguments in strings in tcl. Instead compose your command as a list and then use the {*} operator to expand the list when you call exec:
set cmd [list tclsh script.tcl]
exec {*}$cmd
*Note: There may be confusion about this because in interactive mode tcl DOES indeed launch executables if you type it. But this is only a feature of interactive mode.
Related
It is possible to call other programs from Tcl using the Tcl exec function. Let’s use this
command to create a Tcl script that will take all of the Tcl code you have written so far and
create a single PDF from it. The filename of the single PDF file should be TCL CODE.pdf.
Recall that the Tcl foreach command makes it very easy to go through a list of items.
You should use the exec command to call the Linux program called enscript to produce
a postscript file for each of the Tcl files. You should then use the Linux program called
ps2pdf to turn each of the postscript files into a PDF. Finally, merge all of the PDFs using
the Linux program called gs (short for ghostscript). Invoke ghostscript using the following
command
Some documentation
Tcl documentation
the exec man page
Tcl tutorial
the wiki entry for exec
Within tclsh I can run the following and get the expected output:
% exec bash -c "ulimit -v"
50331648
However within a Tcl script nothing is returned. No error, no output, nothing. There's clearly some gotcha with exec'ing 'bash -c' that I can't work out.
Alternatively, is there a native way in Tcl that I can get the system's memory limit to avoid having to do it this way in the first place?
In an interactive tclsh session, the REPL helpfully prints the output of commands/expressions. That's not the case in a non-interactive program.
exec returns the output of the command: you just need to capture it with the usual command substitution:
set output [exec bash -c "ulimit -v"]
puts $output
The code that you wrote should work; I can't identify why bash would silently fail to run ulimit -v. Even if the script was running in an environment where that was privileged information (why!?) one would still expect to get an error message of some form. That's a very weird problem!
Tcl's base command set doesn't expose any access to memory limits, whether for reading or writing. The simplest workaround that doesn't call an external program is the tclbsd package (apparently it mostly works on most other Unixes as well), which exposes a command that should help:
package require BSD
set limit [bsd::rlimit get soft virtual]
I am launching command prompt as:
eval exec [auto_execok start] &
It returns me the pid, and launches command prompt.
Can I control the launched shell? Or is there any other way.
Right now I am sending command at the time of launch like:
eval exec [auto_execok start] "cd Desktop" &
But I want to control the shell for further operations.
You can't control anything launched that way (other than whatever you can do with the pid); it's specifically requesting to have no control at all with the & at the end.
Some programs can be controlled somewhat when they are launched as pipelines.
set pipeline [open |cmd "r+"]
fconfigure $pipeline -buffering line -blocking 0
puts $pipeline "dir"
while {![fblocked $pipeline]} {
set line [gets $pipeline]
puts "I have read: $line"
}
Even more control can be done via Expect, an extension package.
However, the command prompt window can't be controlled by either of these mechanisms; most programs that open windows can't.
I suspect you're trying to reinvent a batch file the hard way.
If you need to perform a series of tasks using cmd.exe, spawn it using
set cmd [open |cmd.exe "r+"]
and then simply write your batch script to that stream:
puts $cmd $myscript
To explain: shells (Tcl's own tclsh and wish included) usually have two modes of execution: interactive and non-interactive. The fisrt is enabled when the shell is started "as is", the second—when it's started in a pipeline1. In interactive mode, the shell would display you its prompt and accept commands—interactively.
In a non-interactive mode, it will just read commands on its standard input stream and execute them as it reads them.
The cmd.exe of Windows is no exception, so you can open |cmd.exe it in "read/write" mode (r+) and write the script composed in its batch command language to its standard input stream which will be bound to the stream object open returns.
If a process started with open or exec writes anything to its standard error stream and/or quits with a non-zero exit code, those commands will raise an exception (that is, error handling is mostly covered for you).
1 Well, for Windows, it's harder to define what "interactive" vs "non-interactive" means, but it's somewhat irrelevant to the question at hand.
Is it possible to run some interactive shell from inside tclsh? Obviously it's easy to run an interactive shell such as bash or tclsh from inside bash, but I have not found a way to do the reverse.
If I run "exec tclsh" or "exec bash" from inside tclsh I don't get a prompt until I type "exit" and hit enter, or I use "ctrl-C" which kills the parent tclsh.
I would prefer not to use an external package, if at all possible.
Provided you don't want to pass values (other than the exit code) back to the calling Tcl code you can do it pretty easily by redirecting the standard channels so that Tcl doesn't capture them:
exec tclsh <#stdin >#stdout 2>#stderr
This will work for pretty much any subprocess (I've just tested it with vi) and is what tclsh actually does magically for you in interactive mode if it decides to try running a subprocess.
If you want to do anything more complex than that, you probably need to look into using Expect as there's a very long list of tricky gotchas otherwise.
I don't program in TCL but I do use them such as tkcvs and tkdiff. I notice that they declare themselves as shell script
#!/bin/sh
#-*-tcl-*-
What's more, running them through tclsh doesn't work either and I get error like this:
Initialization failed
The second line in the header baffles me too because AFAIK, shell only looks at the #! line. How's this working?
Tcl scripts are normally setup to run using slightly more than you have shown. It is typically and most robustly done like the following:
#!/bin/sh
# \
exec tclsh "$0" ${1+"$#"}
They use the shell initially because there was no standard installation location for tcl so the location could not be relied on. Instead, by starting the system shell and using that to start the tclsh executable you could be certain to run the installed tclsh as long as it was present on the PATH. The shell evaluates the script and sees the exec tclsh "$0" which causes it to execute the installed tclsh binary and pass it $0 (the script file name) as the first argument, which re-runs the script using the tcl interpreter.
The second line in my example comments out the third line when the script is evaluated by the tcl interpreter. The backslash causes the second and third lines to be treated as a single comment when read by tclsh so that tcl doesn't try and run the exec again.
In your snipped the # -*-tcl-*- is marker to indicate the mode to be used by emacs when editing the file. See the documentation.
There is not really enough to go on for the error message. It doesn't seem to be from the Tcl interpreter itself. (That is 'git grep' in the tcl sources does not match that string).