Tcl: Get stdout from exec bash - tcl

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]

Related

Fail to launch tclsh from tclsh

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.

Control windows command prompt via tcl

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.

How do I run interactive shell such as "exec tclsh" from inside tclsh?

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.

rsync-path in expect script

I have written a backup script that uses expect to automate rsync.
To make sure all files get backed up, I use rsync's --rsync-path="sudo rsync" option.
#!/bin/bash
set -e
expect <<- DONE
spawn rsync --rsync-path="sudo\\ rsync" -uav myuser#example.com:/home/myuser/ /backups/home/myuser
expect ":"
send -- "mypassword\r"
expect eof
DONE
This does not work as intended. I get the following error message:
bash: sudo rsync: command not found
rsync: connection unexpectedly closed (0 bytes received so far) [Receiver]
rsync error: error in rsync protocol data stream (code 12) at io.c(226) [Receiver=3.1.1]
I have seen similar questions with respect to using spaces in a rsync command line, and have added single quotes, double quotes, and escape backslashes here and there, but nothing has worked yet.
How do I make "--rsync-path with spaces" work within an expect script?
The problem is that you've got this:
--rsync-path="sudo\\ rsync"
Inside Expect/Tcl, this is seen as:
--rsync-path="sudo rsync"
And, because Tcl's quoting rules are not the same as bash's, that then uses "sudo rsync" with the double quotes as the command to send to the remote side. Which confuses things terribly. The correct fix is to omit the double quotes; the (backslash-quoted) backslash will ensure that it all gets into spawn as one argument, and gets sent to the other side correctly.
I really don't like using HEREdocs with Tcl. Too many things can go weird when different sorts of quoting interact. It's much better to use a single script in the real target language, since then you can use variables to make things clearer:
#!/usr/bin/env expect
set remoteRsync "sudo rsync"
set from myuser#example.com:/home/myuser/
set to /backups/home/myuser
set pass "mypassword"
spawn rsync --rsync-path=$remoteRsync -uav $from $to
expect ":"
send -- "$pass\r"
expect eof
exit
This makes the structure of the code much simpler to see, and easier to debug. The bit with /usr/bin/env at the start is just a way to avoid having the bash wrapper.
And no, those variables won't need quoting at use. Tcl is not bash.
You can't use --rsync-path to do that, since what you want is word-splitting, i.e. something the shell does.
So how can you run a command that runs a command by specifying a single pathname?
On the remote system, write a script wrapper susync doing the sudo (don't forget to chmod 755):
#!/bin/sh
exec /path/to/sudo /path/to/rsync "$#"
and use
spawn rsync --rsync-path=/path/to/susync ...

Why tcl script runs as shell script?

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).