Preface
Goal: A practical case to collect unique record fields using TCL/TK.
Just like anyone else, I don’t know anything about TCL/TK. What is it use for, except for expect test suite in LFS. But I’m curious, so here it is anyway.
Reference Reading
I never read any TCL documentation, until the time this article written.
Source Examples
You can obtain source examples here:
Common Use Case
Task: Get the unique tag string
Please read overview for more detail.
Prepopulated Data
Songs and Poetry
set ::MySongs::Songs [list \
[ dict create \
title "Cantaloupe Island" \
tags [list "60s" "jazz"] ] \
[ dict create \
title "Let It Be" \
tags [list "60s" "rock"] ] \
[ dict create \
title "Knockin' on Heaven's Door" \
tags [list "70s" "rock"] ] \
[ dict create \
title "Emotion" \
tags [list "70s" "pop"] ] \
[ dict create title "The River" ]
];
TCL Solution
The Answer
I use conventional building blocks, loop and conditional.
set tags {}
foreach song $songs {
if [dict exist $song tags] {
foreach tag [dict get $song tags] {
if {[lsearch $tags $tag] < 0} {
lappend tags $tag
}}}}
puts "[join $tags ":"]"
Enough with introduction, at this point we should go straight to coding.
Environment
The command is tclsh
.
No need any special setup. Just run and voila..!
1: Data Structure Using Dictionary
We utilize list
,
throught out this article.
Simple List
Consider begin with simple list
.
#!/usr/bin/env tclsh
set tags [list "rock" "jazz" "rock" "pop" "pop"];
puts $tags;
puts [lindex $tags 1]
set tagsstr [join $tags :];
puts "$tagsstr\n";
puts [llength $tags]
It is easy to dump list
in tcl
using join
.
With the result similar as below list
:
❯ tclsh 01-tags.tcl
rock jazz rock pop pop
jazz
rock:jazz:rock:pop:pop
5
Notice how the assignment written in TCL.
set tagsstr [join $tags ":"];
Shebang
With shebang
such as #!/usr/bin/env tclsh
,
you do not need to type tcl
in CLI.
Instead of
❯ tclsh 02-record.tcl
You can just type
❯ ./02-record.tcl
And you can run the script directly in your text editor.
Such as using geany, you just can hit the F5
key to run the script.
Hash
We can use hash
to store our record.
set song [ \
dict create \
title "Cantaloupe Island" \
tags [list "60s" "jazz"] \
]
And examine how to access the array inside the hash
.
puts $song
set title [dict get $song title]
puts $title
set tags [dict get $song tags]
puts $tags
puts [lindex $tags 0]
Now, examine the result:
title {Cantaloupe Island} tags {60s jazz}
Cantaloupe Island
60s jazz
60s
The Songs Structure
We can continue our journey to records using list
of hash
.
No need any complex structure.
set songs [list \
[ dict create \
title "Cantaloupe Island" \
tags [list "60s" "jazz"] ] \
[ dict create \
title "Let It Be" \
tags [list "60s" "rock"] ] \
[ dict create \
title "Knockin\' on Heaven\'s Door" \
tags [list "70s" "rock"] ] \
[ dict create \
title "Emotion" \
tags [list "70s" "pop"] ] \
[ dict create title "The River" ]
];
Then process in a loop to produce desired output.
puts [llength $songs]
foreach song $songs {
puts $song
}
With the result similar as below record:
❯ ./03-songs.tcl
5
title {Cantaloupe Island} tags {60s jazz}
title {Let It Be} tags {60s rock}
title {Knockin' on Heaven's Door} tags {70s rock}
title Emotion tags {70s pop}
title {The River}
2: Separating Module
Since we need to reuse the songs record multiple times, it is a good idea to separate the record structure from logic.
We require two files.
- One or more module, such as
MySongs.tcl
. - Package Index:
pkgIndex.tcl
Songs Module
The header for MySongs
module should be as below:
namespace eval ::MySongs {
namespace export Songs
set version 1.0
set MyDescription "MySongs"
variable home [file join [pwd]\
[file dirname [info script]]]
}
Then the code details, can be shown as below:
set ::MySongs::Songs [list \
[ dict create \
title "Cantaloupe Island" \
tags [list "60s" "jazz"] ] \
[ dict create \
title "Let It Be" \
tags [list "60s" "rock"] ] \
[ dict create \
title "Knockin' on Heaven's Door" \
tags [list "70s" "rock"] ] \
[ dict create \
title "Emotion" \
tags [list "70s" "pop"] ] \
[ dict create title "The River" ]
];
package provide MySongs $MySongs::version
package require Tcl 8.0
Package Index
We still need to configure package index for MySongs
module.
package ifneeded MySongs 1.0 \
[list source [file join $dir MySongs.tcl]]
Module in Relative Path
In order to use module in relative path, we need to add a few more header declaration.
lappend auto_path "./"
package require MySongs 1.0
Then we can just dump the $Songs
variable.
#!/usr/bin/env tclsh
lappend auto_path "./"
package require MySongs 1.0
puts $MySongs::Songs
With the result as below image:
3: Finishing The Task
Extract, Flatten, Unique
We can solve, this task,
by using imperative approach,
such as for
loop and if
conditional.
Extracting Hash
Filtering hash, and push to array
Consider start with this simple code:
#!/usr/bin/env tclsh
lappend auto_path "./"
package require MySongs 1.0
foreach song $MySongs::Songs {
if [dict exist $song tags] {
set tagss [dict get $song tags]
puts $tagss
}
}
With the result of list
of list
, as shown below.
❯ ./05-extract.tcl
60s jazz
60s rock
70s rock
70s pop
Flatten Module
Since we are going to reuse the flatten approach above in other script. It is better to bundle the script in its own tcl module.
namespace eval ::MyHelperFlatten {
namespace export unique
set version 1.0
set MyDescription "MyHelperFlatten"
variable home [file join [pwd]\
[file dirname [info script]]]
}
proc flatten {songs} { ... }
package provide MyHelperFlatten $MyHelperFlatten::version
package require Tcl 8.0
The return values is an array.
Package Index
We still need to configure package index for MySongs
module.
package ifneeded MySongs 1.0 \
[list source [file join $dir MySongs.tcl]]
package ifneeded MyHelperUnique 1.0 \
[list source [file join $dir MyHelperUnique.tcl]]
Flatten Function
To flatten the code above, we can directly push array with this code below:
proc flatten {songs} {
set tags [list]
foreach song $songs {
if [dict exist $song tags] {
set tagss [dict get $song tags]
foreach tag $tagss {
lappend tags $tag
}
}
}
return $tags
}
Using Flatten Module
There is nothing to say in this code below.
Just apply flatten
function to our song records.
#!/usr/bin/env tclsh
lappend auto_path "./"
package require MySongs 1.0
package require MyHelperFlatten 1.0
set tags [flatten $MySongs::Songs]
set tagsstr [join $tags ":"]
puts "$tagsstr"
With the result of a flattened array
shown below.
❯ ./06-flatten.tcl
60s:jazz:60s:rock:70s:rock:70s:pop
Unique
To solve unique
array,
we can directly check unique value whule filtering.
#!/usr/bin/env tclsh
lappend auto_path "./"
package require MySongs 1.0
set tags [list]
foreach song $MySongs::Songs {
if [dict exist $song tags] {
set tagss [dict get $song tags]
foreach tag $tagss {
if {[lsearch $tags $tag] < 0} {
lappend tags $tag
}}}}
set tagsstr [join $tags ":"];
puts "$tagsstr";
With the result similar as below array:
❯ ./07-unique.tcl
60s:jazz:rock:70s:pop
What is Next 🤔?
We have alternative way to extract the record structure.
Consider continue reading [ TCL - Playing with Records - Part Two ].