I have a list variable $a which has below as value.
{1|Katy|347689 2|Jerry|467841 1|Katy|987654}
I am trying to remove duplicated on the basis of
1|Katy avoiding the userid available at last.
Expected output should be.
{1|Katy|347689 2|Jerry|467841}
I tried using lsort -unique option. Seems like this does not work properly in my case.
set uniqueElement [lsort -unique $a]
Also ,just for illustrative purpose the list values are shown as having 3 values. I have more than 500 in the same format. I am trying to remove duplicates on the basis of 1|Katy while avoiding the userid.
Can suggest any other way I can resolve this to remove duplicates in this format for a list?
This is a little bit tricky as you have parts that are to be ignored when deduplicating. Because of that, lsort -unique is not the right tool. Instead, you want to use a dictionary.
# Identify the values that each key maps to
set d {}
foreach entry $inputList {
# Alternatively, use regular expressions to do the entry parsing
set value [join [lassign [split $entry "|"] a b] "|"]
set key [string cat $a "|" $b]
dict lappend d $key $value
}
# Build the output list using the first value each key maps to
set outputList {}
dict for {key values} $d {
lappend outputList [string cat $key "|" [lindex $values 0]]
}
That makes outputList hold the value you are seeking. (You don't need to use string cat, but I think it makes the code clearer in this case.)
You can still use lsort -unique by manipulating your initial list beforehand:
set new_format_list [join [split $a "|"] ]
set new_format_sorted_list [lsort -unique -stride 3 $new_format_list]
foreach {el1 el2 el3} $new_format_sorted_list {
lappend newlist "$el1|$el2|$el3"
}
puts "$newlist"
The variable new_format_list is now a flat list of all elements of your entry list (here, 9 elements). The | have been used to split the element of your initial list.
The variable new_format_sorted_list actually remove duplicate. Stride 3 means elements of the list will be check 3 by 3. Only the 1st of 3 is used for comparison.
The foreach is used to create a list with the same format that is used in entry. lappend is able to create variable if it doen't exist.
check the result
Normally, you should get what you want.
Edit based on nurdglaw pertinent comment
# entry list
set original_list {1|Katy|347689 2|Jerry|467841 1|Katy|987654}
set temp_list [join [split $original_list "|"] ]
# dirty method
# separate the third element from the first two
# then the string of the first two elements is the id for uniqueness
foreach {l1 l2 l3} $temp_list {
append new_format_list "$l1|$l2 $l3 " ;# use string to make a tcl list
#the space at this end is mandatory
}
set new_format_sorted_list [lsort -unique -stride 2 $new_format_list]
foreach {el1 el2} $new_format_sorted_list {
lappend cleanlist "$el1|$el2"
}
puts "$cleanlist"
Related
I am trying to match something and if it matches set it to a variable for later use and printing is optional.
set pl m4
set ml m14
set match_name ABC_XYZ_${pl}_$ml
set list1 {ABC_XYZ_m0_m5_1_1_1_1 ABC_XYZ_m4_m14_1_1_1_1_1_1_1_1_1_1 ABC_XYZ_m0_m14_1_1_1_1_1_1}
set found ""
foreach x $list1 {
if {[regexp $match_name $list1]} {
set found $x
puts $found
}
break
}
The problem with the above code is that, it sets found to 1st element of the list because of match. This code only works if my match_name is first element of list.
Please correct it or suggest alternative solution. Note ABC_XYZ_ will always remain same. and "pl" will always change as it is dynamic
Note: Reason , I tried break command is to exit the loop when we have the match
I tried something with
lsearch -regexp $list1 $match_name but did not work
I figured out the solution: (NO Need of foreach loop)
set pl m4
set ml m14
set match_name ABC_XYZ_${pl}_$ml
set list1 {ABC_XYZ_m0_m5_1_1_1_1 ABC_XYZ_m4_m14_1_1_1_1 ABC_XYZ_m0_m14_1_1_1_1_1_1_1_1}
set found ""
set found [lsearch -inline -regexp $list1 $match_name]
puts $found
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.
I'm newbie to Tcl. I have a list like this:
set list1 {
dir1
fil.txt
dir2
file.xls
arun
baskar.tcl
perl.pl
}
From that list I want only elements like:
dir1
dir2
arun
I have tried the regexp and lsearch but no luck.
Method 1:
set idx [lsearch $mylist "."]
set mylist [lreplace $mylist $idx $idx]
Method 2:
set b [regsub -all -line {\.} $mylist "" lines]
Method 1 would work if you did it properly. lsearch returns a single result by default and the search criteria accepts a glob pattern. Using . will only look for an element equal to .. Then you'll need a loop for the lreplace:
set idx [lsearch -all $list1 "*.*"]
foreach id [lsort -decreasing $idx] {
set list1 [lreplace $list1 $id $id]
}
Sorting in descending order is important because the index of the elements will change as you remove elements (also notice you used the wrong variable name in your code snippets).
Method 2 would also work if you used the right regex:
set b [regsub -all -line {.*\..*} $list1 ""]
But in that case, you'd probably want to trim the results. .* will match any characters except newline.
I would probably use lsearch like this, which avoids the need to replace:
set mylist [lsearch -all -inline -not $list1 "*.*"]
The lsearch command has many options, some of which can help to make this task quite easy:
lsearch -all -inline -not $list1 *.*
Alternatively, filter the list for unaccepted elements using lmap (or an explicit loop using foreach):
lmap el $list1 {if {[string first . $el] >= 0} {continue} else {set el}}
See also related discussion on filtering lists, like Using `lmap` to filter list of strings
I've a command output as something below this is an example
card-1-1-1 4 -Number 1 -type Eth -config -GEPorts 4
card-1-3-1 3 -Number 2 -type Eth -config Yes -GEPorts 3
I need this to be converted into a list like
card-1-1-1 4
-Number 1
-type Eth
-config if_empty_insert_null
-GEPorts 4
card-1-3-1 3
-Number 2
-type Eth
-config Yes
-GEPorts 3
Well, if it wasn't for the fact that you've got some options that are sometimes missing associated values, this would be pretty much trivial. As it is, we need to be more careful. The main tricky bits are using regexp -all -inline to parse to a Tcl list and using a for loop to iterate over everything when detecting absent parameters.
# Process each line
foreach row [split $inputData "\n"] {
# If there's a comment syntax or blank lines are allowed, you handle them here
# Safely convert to a Tcl list
set words [regexp -all -inline {\S+} $row]
# First two words are used "as is"
set pairs [lrange $words 0 1]
# Can't use foreach here; non-constant step size prevents it
for {set i 2} {$i < [llength $words]} {incr i} {
set paramName [lindex $words $i]
set next [lindex $words [expr {$i + 1}]]
# Set the default for if the option is value-less
set parameter "if_empty_insert_null"
# Look for a value; slightly complex as I'm allowing for negative numbers
if {$next ne "" && ![regexp {^-[a-zA-Z]} $next]} {
set parameter $next
incr i
}
# Now we can update the list as we know the pair of values to add
lappend pairs $paramName $parameter
}
# Now print everything out; we can use foreach for this as we're guaranteed to
# have an even number of values
foreach {a b} $pairs {
# Do more complex formatting if you want
puts "$a $b"
}
}
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