pass a string which contains variables to a proc in tcl, without evaluation - tcl

I want to pass a string to a proc which contains variables, without evaluating/substituting the value of the variable for example :
% set intf "blah"
% set cmd "show router interface $intf"
% dummy_proc $cmd
the requirement is that, the $cmd that dummy_proc receives should be "show router interface $intf" and not "show router interface blah"
I am looking for answers which do not involve the following :
using a backslash for the $ => \$
wrapping the string in curly braces {}
I'd appreciate any help with this. Thanks

The problem is that Tcl uniformly substitutes those bits before you procedure gets them; it's how the language is defined to operate. The sole extra piece of information you can get is this (if your version of Tcl is new enough):
proc example {cmd} {
puts call=[dict get [info frame -1] cmd]
puts cmd=$cmd
}
set intf "blah"
set cmd "show router interface $intf"
example $cmd
which will print:
call=example $cmd
op=show router interface blah
The setting of cmd is already over and done with at the point you get to inspect.
The way you'd solve this with slightly freer requirements is:
proc example {cmd} {
puts "supplied: $cmd"
# Do the substitutions as in "double quotes" *in the caller's context*
set cmd [uplevel 1 [list subst $cmd]]
puts "substituted: $cmd"
}
set intf "blah"
set cmd {show router interface $intf}
example $cmd
which prints:
supplied: show router interface $intf
substituted: show router interface blah
You cannot solve this without delaying the substitution of $intf until the evaluation of the procedure, and that requires either braces or the (ugly) strategies listed in the other answers. Tcl style genuinely encourages the use of braces for this sort of thing, and double quotes always mean that variables (and command calls) inside are to be substituted immediately. Delay that substitution (which you have the tools for) and you can substitute at the moment you need.

Simple. For example, you could use one of these:
set cmd "show router interface \x24intf"
set cmd [format "show router interface $%s" intf]
append cmd "show router interface " $ intf
set cmd [string cat "show router interface " $ intf]
But I expect that isn't what you wanted either.
Tcl provides an eminently simple and expressive way to specify a minimally-substituted string:
set cmd {show router interface $intf}
This has the advantages of clarity and of not complicating the interpreter with exceptions from the straightforward evaluation rules that Tcl uses.
Documentation:
Summary of Tcl language syntax

There are lots of ugly alternatives to using braces, for example:
set cmd [string map "% $" "show router interface %intf"]
set cmd [join [list "show router interface $" "intf"] ""]
set cmd [format "show router interface %sintf" $]
set cmd "show router interface [set $ $]intf"
set cmd $; set cmd "show router interface [set cmd]intf"
set cmd "show router interface [lindex $ 0]intf"
Why you would want to use these alternatives instead of braces is a mystery.

Related

Reading cmd arguments in TCL file

I am trying to run a tcl script through .bat file. I want to read some cmd arguments in the tcl script. Below is my code:
Command to run:
D:\Cadence\Sigrity2021.1\tools\bin\PowerSI.exe -tcl abcd.tcl %new_var%.spd %new_file_name%
Below is how I am trying to read the variable in the tcl file:
sigrity::open document [lindex $argv 0] {!}
It open up the Cadence Sigrity, but I see the below error:
How do I read cmd argument in tcl?
If you have no other way to do it that you can find (and it sounds like that might be the case) then you can fake it by writing a helper file with content like this, filling in the real arguments in the appropriate places:
# Name of script to call
set ::argv0 "abcd.tcl"
# Arguments to pass
set ::argv {}
lappend ::argv "%new_var%.spd"
lappend ::argv "%new_file_name%"
# Number of arguments (rarely used)
set ::argc [llength $::argv]
# Do the call
source $::argv0
Then you can pass that file to PowerSI and it will set things up and chain to the real file. It's messy, but practical.
If you're writing this from Tcl, use the list command to do the quoting of the strings (instead of putting them in double quotes) as it will do exactly the right thing for you. If you're writing the file from another language, you'll want to make sure you put backslashes in before \, ", $ and [ characters. The fiddlyness of doing that depends on your language.

tcl open pipe seems to misshandle spaces in parameters

I have this open:
set r [catch {open "|[concat $config(cmd,sh) [list $cmd 2>#1]]" r} fid]
where $config(cmd,sh) is cmd /c and I am trying to pass a file name (and possibly a command such as echo) in $cmd. If there is no space in the file name, i.e. :
cmd is echo /filename
all is well. With a space, i.e.:
cmd is echo "/file name"
what appears to be passed is:
\"file name\".
When I try this on Linux, I get "file name" (no backslashes). I have tried replacing the spaces in the file name with "\ ", but then the target gets two file names, i.e. the space is used to break up the file name.
I am beginning to think I have found a bug in the Windows port of Tcl...
Ugh, that looks convoluted! To pass this sort of thing into the pipe creation code, you need to use exactly the right recipe:
set r [catch {open |[list {*}$config(cmd,sh) $cmd 2>#1] r} fid]
That is, always use the form with |[list ...] when building pipes as the documentation says that is what the pipe opener looks for. (This is the only command like that in Tcl.)
And of course, using the (8.5+) {*} syntax is much simpler in this case too, as it is more obviously doing the right thing.

Getting unevaluated tcl arguments

What I want to do is parse an argument to a tcl proc as a string without any evaluation.
For example if I had a trivial proc that just prints out it's arguments:
proc test { args } {
puts "the args are $args"
}
What I'd like to do is call it with:
test [list [expr 1+1] [expr 2+2]]
And NOT have tcl evaluate the [list [expr 1+1] [expr 2+2]]. Or even if it evaluated
it I'd still like to have the original command line. Thus with the trivial "test"
proc above I'd like to be able to return:
the args are [list [expr 1+1] [expr 2+2]]
Is this possible in tcl 8.4?
You cannot do this with Tcl 8.4 (and before); the language design makes this impossible. The fix is to pass in arguments unevaluated (and enclosed in braces). You can then print them however you like. To get their evaluated form, you need to do this inside your procedure:
set evaluated_x [uplevel 1 [list subst $unevaluated_x]]
That's more than a bit messy!
If you were using Tcl 8.5, you'd have another alternative:
set calling_code [dict get [info frame -1] cmd]
The info frame -1 gets a dictionary holding a description of the current command in the context that called the current procedure, and its cmd key is the actual command string prior to substitution rules being applied. That should be about what you want (though be aware that it includes the command name itself).
This is not available for 8.4, nor will it ever be backported. You might want to upgrade!
When passing the arguments into test, enclose them in braces, e.g.:
test {[list [expr 1+1] [expr 2+2]]}

Manipulate non-global variables from fileevent handler

Is there a way to manipulate non-global variables from a fileevent handler? Consider the following minimal server:
proc initState {stateName} {
upvar $stateName state
set state(foo) bar
set state(baz) bla
# ...
return
}
proc handleConnection {stateName newsock clientAddress clientPort} {
upvar $stateName state
fconfigure $newsock -blocking 0
fconfigure $newsock -buffering line
fileevent $newsock readable [list handleData $newsock]
return
}
proc handleData {f} {
if {[eof $f]} {
fileevent $f readable {}
close $f
return
}
gets $f line
puts $f ok
# need to modify state here...
return
}
proc runServer {port} {
array set state {}
initState state
socket -server {handleConnection state} $port
vwait forever
}
runServer 1234
Is there any possibility to manipulate the state array created in the scope of runServer or is the only way to do this making state a global variable?
I'm pretty new to Tcl, if I were using C I would simply pass a pointer to state into the event handler but unfortunately Tcl does not allow that. Am I doing anything weird here, is there a more Tcl-ish way?
That's simply not going to work. The issue is that Tcl's stack frames do not persist in the way that what you want would require.
The standard options to work around this are:
Keep the state in a global array that is indexed by a "connection token" (e.g., the name of the channel). Remember that arrays are indexed by strings; composite keys like “sock42,hostname” are quite legal.
Keep the state in a namespace named after the connection token. If you're using Tcl 8.5, the namespace upvar command makes this much easier.
Keep the state in a TclOO object (requires Tcl 8.6 or the separate TclOO package for 8.5) or use a different object system (e.g., [incr Tcl], XOTcl; these are available for many Tcl versions).
Keep the state in a coroutine (requires Tcl 8.6). This effectively gives you a named stack (and lets you write your code so it is apparently “straight line” instead of driven by callback) but its version requirement is strict.

How do I use the eval statement in tcl

So basically in my tcl script I generate a line of tcl code that I have to execute( ie I come up with this instruction during runtime). Once I have the tcl instruction say for example puts "hello world $test_var". How do I execute this using tcl?
Do I use the eval command or what?
The eval command is a reasonable way of doing this, but you might want to consider using catch instead, since that will trap any problems found during the evaluation of the generated code, for example like this:
# Generate the code somehow
set variable {puts "hello word $test_var"}
# Execute and trap problems
if {[catch $variable resultMsg]} {
# An error (or other exception) happened
puts stderr "BOOM, Error! $resultMsg"
}
Instead of using [eval] which works perfectly well, in newer versions of Tcl you can use the {*} syntax.
For example:
set cmd "puts"
lappend cmd "abcd ef"
{*}$cmd
Note that it's a good habit to use list commands to build up your command so you wont run into quoting issues.
I'm afraid you don't have another reasonable choice than to use the eval command, e.g.:
set variable {puts "hello world $test_var"}
eval $variable