Tcl adds curly braces when using `$` sign - list

set B {pc_0::!mx_0 pi::$mx_0}
puts $B
set A ""
foreach x $B {
lappend A $x
}
puts $A
The output of this program is
pc_0::!mx_0 pi::$mx_0
pc_0::!mx_0 {pi::$mx_0}
It is strange that tcl adds curly braces in second output. I guess it is because it uses $ symbol. But I really need to use it and I don't want the braces to be inserted. How this can be explained and how to avoid the braces?

As a general rule, don't treat lists as strings. Pretend that they don't have a string representation. (The string representation is only useful for serialization, debugging, but not for the user).
To convert text (especially user-input) to a list use split.
To convert it back, use join.
Sou you want:
puts [join $A]
Background:
A list have the sideeffect of escaping all meta-characters used by Tcl so no further subsitution takes place when you eval this list. This is a very important property for generating Callbacks/code that will be later executed:
set userinput [gets stdin]
set code [list puts $userinput]
eval $code
No matter what the user enters here, the output is always the same as the user entered, without any substitution.
If the $ would not be escaped, then an evaluation would try to substitute $mx_0, which will most likly fail.

Why not print the list in a way similar to how it was created?
As an experienced programmer new to Tcl, this seems much more intuitive to me:
foreach x $A {
puts $x
}

Related

TCL Split string by special character

I have a string in tcl say:
set name "a_b_c_d"
and I want to get 4 variables out of it like $a would have the value, $b the value b, etc...
Thanks a lot !
This is exactly what the split command is for. You just need to provide the optional argument that says what character to use to split the string into a list of its fields.
set fields [split $name "_"]
Note that if you have two of the split character in a row, you get an empty list element in the result.
Your requirement is a bit strange in my opinion, but that's how I would do it:
set name a_b_c_d
foreach item [split $name "_"] {
set $item $item
}
You didn't ask for the following, but I believe it might be better if you use an array, so you know exactly where your variables are, instead of just being 'there' in the open:
set name a_b_c_d
foreach item [split $name "_"] {
set items($item) $item
}
parray items
# items(a) = a
# items(b) = b
# items(c) = c
# items(d) = d
EDIT: Since you mentioned it in a comment, I'll just put it here: if the situation is as you mentioned, I'd probably go like this:
lassign [split $name "_"] varName folderName dirName
And it should still work most of the time. Dynamic variable names are not recommended and can 90% of the time be avoided for a safer, more readable and maintainable code. Sure, it works for things that you just need once in a blue moon, but you need to know what you are doing.

regexp tcl to search for variables

I am trying to find the matching pattern using regexp command in the {if loop} . Still a newbie in tcl. The code is as shown below:
set A 0;
set B 2;
set address "my_street[0]_block[2]_road";
if {[regexp {street\[$A\].*block\[$B\]} $address]} {
puts "the location is found"
}
I am expecting the result to return "the location is found" as the $address contain matching A and B variables. i am hoping to able to change the A and B number for a list of $address. but I am not able to get the result to return "the location is found".
Thank you.
Tcl's regular expression engine doesn't do variable interpolation. (Should it? Perhaps. It doesn't though.) That means that you need to do it at the generic level, which is in general quite annoying but OK here as the variables only have numbers in, which are never RE metacharacters by themselves.
Basic version (with SO. MANY. BACKSLASHES.):
if {[regexp "street\\\[$A\\\].*block\\\[$B\\\]" $address]} {
Nicer version with format:
if {[regexp [format {street\[%d\].*block\[%d\]} $A $B] $address]} {
You could also use subst -nocommands -nobackslashes but that's getting less than elegant.
If you need to support general substitutions, it's sufficient to use regsub to do the protection.
proc protect {string} {
regsub -all {\W} $string {\\&}
}
# ...
if {[regexp [format {street\[%s\].*block\[%s\]} [protect $A] [protect $B]] $address]} {
It's overkill when you know you're working with alphanumeric substitutions into the RE.

Perl regular expressions troubles

I have a variable $rowref->[5] which contains the string:
" 1.72.1.13.3.5 (ISU)"
I am using XML::Twig to build modify an XML file and this variable contains the information for the version number of something. So I want to get rid of the whitespaces and the (ISU). I tried to use a substitution and XML::Twig to set the attribute:
$artifact->set_att(version=> $rowref->[5] =~ s/([^0-9\.])//g)
Interestingly what I got in my output was
<artifact [...] version="9"/>
I don't understand what I am doing wrong. I checked with a regular expression tester and it seems fine. Can somebody spot my error?
The return value of s/// is the number of substitutions it made, which in your case is 9. If you are using at least perl 5.14, add the r flag to the substitution:
If the "/r" (non-destructive) option is used then it runs the
substitution on a copy of the string and instead of returning the
number of substitutions, it returns the copy whether or not a
substitution occurred. The original string is never changed when
"/r" is used. The copy will always be a plain string, even if the
input is an object or a tied variable.
Otherwise, go through a temporary variable like this:
my $version = $rowref->[5];
$version =~ s/([^0-9\.])//g;
$artifact->set_att(version => $version);
The regex substitution changes the varialbe in place but returns the number of substitutions it made (1 without the /g modifier, if it was succesful).
my $str = 'words 123';
my $ret = $str =~ s/\d/numbers/g;
say "Got $ret. String is now: $str";
You can do the substitution first, $rowref->[5] =~ s/...//;, and then use the changed variable.

Passing a match in regsub with & to a procedure (Tcl is being used)

I want to go through a comma separated string and replace matches with more comma separated elements.
i.e 5-A,B after the regsub should give me 1-A,2-A,3-A,4-A,5-A,B
The following is not working for me as & is being passed as an actual & instead of the actual match:
regsub -all {\d+\-\w+} $string [myConvertProc &]
However not attempting to pass the & and using it directly works:
regsub -all o "Hello World" &&&
> Hellooo Wooorld
Not sure what I am doing wrong in attempting to pass the value & holds to myConvertProc
Edit: I think my initial problem is the [myConvertProc &] is getting evaluated first, so I am actually passing '&' to the procedure.
How do I get around this within the regex realm? Is it possible?
Edit 2: I've already solved it using a foreach on a split list, so I'm just looking to see if this is possible within a regsub. Thanks!
You are correct in your first edit: the problem is that each argument to regsub is fully evaluated before executing the command.
One solution is to insert a command substitution string into the string, and then use subst on it:
set string [regsub -all {\d+\-\w+} $string {[myConvertProc &]}]
# -> [myConvertProc 5-A],B
set string [subst $string]
# -> 1-A,2-A,3-A,4-A,5-A,B
This will only work if there is nothing else in string that is subject to substitution (but you can of course turn off variable and backslash substitution).
The foreach solution is much better. An alternative foreach solution is to iterate over the result of regexp -indices -inline -all, but iterating over the parts of a split list is preferable if it works.
Update:
A typical foreach solution goes like this:
set res {}
foreach elem [split $string ,] {
if {[regexp -- {^\d+-\w+$} $elem]} {
lappend res [myConvertProc $elem]
} else {
lappend res $elem
}
}
join $res ,
That is, you collect a result list by looking at each element in the raw list. If the element matches your requirement, you convert it and add the result to the result list. If the element doesn't match, you just add it to the result list.
It can be simplified somewhat in Tcl 8.6:
join [lmap elem [split $string ,] {
if {[regexp -- {^\d+-\w+$} $elem]} {
myConvertProc $elem
} else {
set elem
}
}] ,
Which is the same thing, but the lmap command handles the result list for you.
Documentation: foreach, lappend, lmap, regexp, regsub, set, split, subst

Case matching regexp

I have been wondering about a regexp matching pattern in Tcl for some time and I've remained stumped as to how it was working. I'm using Wish and Tcl/Tk 8.5 by the way.
I have a random string MmmasidhmMm stored in $line and the code I have is:
while {[regexp -all {[Mm]} $line match]} {
puts $data $match
regsub {[Mm]} $line "" line
}
$data is a text file.
This is what I got:
m
m
m
m
m
m
While I was expecting:
M
m
m
m
M
m
I was trying some things to see how changing a bit would affect the results when I got this:
while {[regexp -all {^[Mm]} $line match]} {
puts $data $match
regsub {[Mm]} $line "" line
}
I get:
M
m
m
Surprisingly, $match keeps the case.
I was wondering why in the first case, $match automatically becomes lowercase for some reason. Unless I am not understanding how the regexp actually is working, I'm not sure what I could be doing wrong. Maybe there's a flag that fixes it that I don't know about?
I'm not sure I'll really use this kind of code some day, but I guess learning how it works might help me in other ways. I hope I didn't miss anything in there. Let me know if you need more information!
The key here is in your -all flag. The documentation for that said:
-all -- Causes the regular expression to be matched as many times as possible in the string, returning the total number of matches found. If this is specified with match variables, they will contain information for the last match only.
That means the variable match contains the very last match, which is a lower case 'm'. Drop the -all flag and you will get what you want.
Update
If your goal is to remove all 'm' regardless of case, that whole block of code can be condensed into just one line:
regsub -all {[MM]} $line "" line
Or, more intuitively:
set line [string map -nocase {m ""} $line]; # Map all M's into nothing