TCL Split string by special character - list

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.

Related

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

Tcl adds curly braces when using `$` sign

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
}

Extract number from a variable in powershell

I am very new to powershell and am trying to extract a number from a variable. For instance, I get the list of ports using this command:
$ports = [system.io.ports.serialport]::getportnames()
The contents of $ports is: COM1, COM2, COM10, COM16 and so on.
I want to extract the numbers from $ports. I looked at this question. It does what I want, but reads from a file.
Please let me know how to resolve this.
Thanks.
Edit: I was able to do what I wanted as follows:
$port=COM20
$port=$port.replace("COM","")
But if there is any other way to do this, I will be happy to learn it.
Well, a quick way would be
$portlist = [System.IO.Ports.SerialPort]::GetPortNames() -replace 'COM'
If you want it to be a list of integers and not numeric strings, then you can use
[int[]] $portlist = ...
Something like this should work:
# initialize the variable that we want to use to store the port numbers.
$portList = #()
# foreach object returned by GetPortNames...
[IO.Ports.SerialPort]::GetPortNames() | %{
# replace the pattern "COM" at the beginning of the string with an empty
# string, just leaving the number. Then add the number to our array.
$portList += ($_ -ireplace "^COM", [String]::Empty)
}
Note that I used [IO.Ports.SerialPort] instead of [System.IO.Ports.SerialPort]. These are the same things - PowerShell implicitly assumes that you're working with the [System] namespace, so you don't need to specify it explicitly, though there's nothing wrong with doing it.
Edit
To answer your questions:
%{...} is shorthand for foreach-object {...}.
$_ indicates the object that is currently in the pipeline. When we're inside a foreach-object block, $_ resolves to the one object out of the entire collection that we're currently dealing with.
If we write my code a bit differently, I think it'll be easier to understand. Between these examples, $_ and $port are the same things.
$portList = #()
foreach ($port in [IO.Ports.SerialPorts]::GetPortNames()) {
$portList += ($port -ireplace "^COM", [String]::Empty)
}
Hope that helps!
This should cover it:
$portnos = $ports | foreach {$_ -replace 'COM',''}

Using Regex/Grep to grab lines from an array using an array of patterns

After searching everywhere on the web, and being a noob to perl, for a solution to this I have decided to post on Stack.
I am looking to do is loop through array1 containing required matches (they will be different each time and could contain lots of patterns (well strings that need to be matched) but using this example so I can understand the problem). Then testing each element against a grep which is using array2 that contains some strings. Then printing out the lines that grep found to match the patterns used.
#!/usr/bin/perl
use strict;
use warnings;
use POSIX qw( strftime );
my (#regressions,#current_test_summary_file,#regression_links);
#regressions = ("test","table");
#current_test_summary_file = ("this is the line for test \n","this is the line for table \n","this is the line for to\n");
foreach (#regressions)
{
print $_ . "\n";
#regression_links = grep(/$_/, #current_test_summary_file);
}
foreach(#regression_links)
{
print $_ . "\n";
}
So would like to pick up only the first two elements instead of all three which is happening now.
Hopefully I've explained my problem properly. I've tried quite a few things (using qq for example) but have only really used grep to try this (unsure how I could do this approach using something different). If someone can point me in the right direction (and whether I should be using grep at all to solve this problem for that matter) I would be very grateful. Just tried this code below instead of just get the second element any ideas anyone ? (sorry can't reply to ur comment some reason but so u know axeman second idea worked).
foreach my $regression (#regressions)
{
print $regression . "\n";
#regression_links = grep(/$regression/, #current_test_summary_file);
}
Inside of grep, $_ refers to the list element involved in the test. Also /abc/ is short for $_ =~ /abc/ so you're effectively testing $_ =~ /$_/ guess what the answer is likely to be (with no metacharacters)?
So you're passing all values into #regression_links.
What you need to do is save the value of $_. But since you're not using the simple print statement, you could just as easily reserve the $_ variable for the grep, like so:
foreach my $reg ( #regressions ) {
print "$reg\n";
#regression_links = grep(/$reg/, #current_test_summary_file );
}
However, you're resetting #regression_links with each loop, and a push would work better:
push #regression_links, grep(/$reg/, #current_test_summary_file );
However, a for loop is a bad choice for this anyway, because you could get duplicates and you might not want them. Since you're matching by regex, one alternative with multiple criteria is to build a regex alternation. But in order to get a proper alternation, we need to sort it by length of string descending and then by alphabetic order (cmp).
# create the alternation expression
my $filter
= join( '|'
, sort { length( $b ) <=> length( $a )
|| $a cmp $b
}
#regressions
);
#regression_links = grep( /$filter/, #current_test_summary_file );
Or other than concatenating a regex, if you wanted to test them separately, the better way would be with something like List::MoreUtils::any:
#regression_links
= grep {
my $c = $_; # save $_
return any { /$c/ } #regressions;
} #current_test_summary_file
;
Axeman is correct and localising $_ with $reg will solve your problem. But as for pulling out matches I would naively push all matches onto #regression_links producing a list of (probably) multiple matches. You can then use List::MoreUtils::uniq to trim down the list. If you don't have List::MoreUtils installed you can just copy the function (its 2 lines of code).
# Axeman's changes
foreach my $reg (#regressions) {
print "regression: $reg\n";
# Push all matches.
push #regression_links, grep(/$reg/, #current_test_summary_file);
}
# Trim down the list once matching is done.
use List::MoreUtils qw/uniq/;
foreach ( uniq(#regression_links) ) {
print "$_\n";
}

changing several expressions in one line in perl

I want to take a line containing several expressions of the same structure, containing 4 digit hexa numbers, and changing the number in that structure according to a hash table. I tried using this next peace of code:
while ($line =~ s/14'h([0-9,a-f][0-9,a-f][0-9,a-f][0-9,a-f])/14'h$hash_point->{$1}/g){};
Where $hash_point is a pointer to the hash table.
But it tells me that I try to use an undefined value, when I tried running the fallowing code:
while ($line =~ s/14'h([0-9,a-f][0-9,a-f][0-9,a-f][0-9,a-f])/14'h----/g){print $1," -> ",$hash_point->{$1},"\n";};
It changed all the wanted numbers to "----" but printed out the values only 2 times (there were much more changes).
Where is the problem?
This is what I used in the end:
$line =~ s/14'h([0-9a-f][0-9a-f][0-9a-f][0-9a-f])/"14'h".$hash_point->{$1}/ge;
and in order to account for numbers not in the hash I've added:
$line =~ s/14'h([0-9a-f][0-9a-f][0-9a-f][0-9a-f])/"14'h".((hash_point->{$1}) or ($1))/ge;
I also wanted to know what numbers don't appear at the hash:
$line =~ s/14'h([0-9a-f][0-9a-f][0-9a-f][0-9a-f])/"14'h".(($hash_point->{$1}) or (print "number $1 didn't change\n") &&($1))/ge;
and finaly, I wanted to be able to control whether the massage from the previous stage would be printed, I've added the use of $flag which in defined only if I want the massages to appear:
$line =~ s/14'h([0-9a-f][0-9a-f][0-9a-f][0-9a-f])/"14'h".(($hash_point->{$1}) or (((defined($flag)) && (print "number $1 didn't change\n")) or ($1)))/ge;
Your regexp seems to work well for me except when hexa number is not present in the hash.
I tried:
#!/usr/bin/perl
use 5.10.1;
use strict;
use warnings;
use Data::Dumper;
my $line = q!14'hab63xx14'hab88xx14'hab64xx14'hab65xx14'hcdef!;
my $hash_point = {
ab63 => 'ONE',
ab64 => 'TWO',
ab65 => 'THREE',
};
while ($line =~ s/14'h([0-9,a-f][0-9,a-f][0-9,a-f][0-9,a-f])/14'h$hash_point->{$1}/g){};
say $line;
This produces:
Use of uninitialized value in concatenation (.) or string at C:\tests\perl\test5.pl line 15.
Use of uninitialized value in concatenation (.) or string at C:\tests\perl\test5.pl line 15.
14'hONExx14'hxx14'hTWOxx14'hTHREExx14'h
The errors are for numbers ab88 and cdef that are not keys in the hash.
Just a small correction, but both of your regexes don't do what you think it does.
/[a-f,0-9]/
Matches any character from a to f, 0 to 9, and a comma. You are looking for
/[a-z0-9]/
Not that this is what is breaking your program (M42 probably got it right, but we can't be sure unless you show us the hash).
Also, apologies, not enough rep to actually answer to other posts.
EDIT:
Well, you go through a lot of hoops in that answer, so here's how I'd do it instead:
s/14'h\K(\p{AHex}{4})/if (defined($hash_point->{$1})) {
$hash_point->{$1};
} else {
say $1 if $flag;
$1;
}/ge
Mainly because chaining and's and &&'s and sosuch generally makes for fairly hard-to-understand code. All whitespace is optional, so squash it for the one-liner!