TCL: Recursively search subdirectories to source all .tcl files - tcl

I have a main TCL proc that sources tons of other tcl procs in other folders and subsequent subdirectories. For example, in the main proc it has:
source $basepath/folderA/1A.tcl
source $basepath/folderA/2A.tcl
source $basepath/folderA/3A.tcl
source $basepath/folderB/1B.tcl
source $basepath/folderB/2B.tcl
source $basepath/folderB/3B.tcl
and it seems kind of stupid to do it that way when I always know I will source everything in folderA and folderB. Is there a function (or simple way) that'll allow me to just source all the .tcl files in an entire folder?

Building on ramanman's reply, heres a routine that tackles the problem using the built in TCL file commands and which works it way down the directory tree recursively.
# findFiles
# basedir - the directory to start looking in
# pattern - A pattern, as defined by the glob command, that the files must match
proc findFiles { basedir pattern } {
# Fix the directory name, this ensures the directory name is in the
# native format for the platform and contains a final directory seperator
set basedir [string trimright [file join [file normalize $basedir] { }]]
set fileList {}
# Look in the current directory for matching files, -type {f r}
# means ony readable normal files are looked at, -nocomplain stops
# an error being thrown if the returned list is empty
foreach fileName [glob -nocomplain -type {f r} -path $basedir $pattern] {
lappend fileList $fileName
}
# Now look for any sub direcories in the current directory
foreach dirName [glob -nocomplain -type {d r} -path $basedir *] {
# Recusively call the routine on the sub directory and append any
# new files to the results
set subDirList [findFiles $dirName $pattern]
if { [llength $subDirList] > 0 } {
foreach subDirFile $subDirList {
lappend fileList $subDirFile
}
}
}
return $fileList
}

It gets trivial with tcllib on board:
package require fileutil
foreach file [fileutil::findByPattern $basepath *.tcl] {
source $file
}

Perhaps a little more platform independent and using builtins commands instead of piping to a process:
foreach script [glob [file join $basepath folderA *.tcl]] {
source $script
}
Repeat for folderB.
If you have more stringent selection criteria, and don't worry about running on any other platforms, using find may be more flexible.

Here is one way:
set includes [open "|find $basedir -name \*.tcl -print" r]
while { [gets $includes include] >= 0 } {
source $include
}
close $includes

Based on a previous answer, this version handles cycles created by symbolic links and in the process eliminates duplicate files due to symbolic links as well.
# findFiles
# basedir - the directory to start looking in
# pattern - A pattern, as defined by the glob command, that the files must match
proc findFiles {directory pattern} {
# Fix the directory name, this ensures the directory name is in the
# native format for the platform and contains a final directory seperator
set directory [string trimright [file join [file normalize $directory] { }]]
# Starting with the passed in directory, do a breadth first search for
# subdirectories. Avoid cycles by normalizing all file paths and checking
# for duplicates at each level.
set directories [list]
set parents $directory
while {[llength $parents] > 0} {
# Find all the children at the current level
set children [list]
foreach parent $parents {
set children [concat $children [glob -nocomplain -type {d r} -path $parent *]]
}
# Normalize the children
set length [llength $children]
for {set i 0} {$i < $length} {incr i} {
lset children $i [string trimright [file join [file normalize [lindex $children $i]] { }]]
}
# Make the list of children unique
set children [lsort -unique $children]
# Find the children that are not duplicates, use them for the next level
set parents [list]
foreach child $children {
if {[lsearch -sorted $directories $child] == -1} {
lappend parents $child
}
}
# Append the next level directories to the complete list
set directories [lsort -unique [concat $directories $parents]]
}
# Get all the files in the passed in directory and all its subdirectories
set result [list]
foreach directory $directories {
set result [concat $result [glob -nocomplain -type {f r} -path $directory -- $pattern]]
}
# Normalize the filenames
set length [llength $result]
for {set i 0} {$i < $length} {incr i} {
lset result $i [file normalize [lindex $result $i]]
}
# Return only unique filenames
return [lsort -unique $result]
}

Same idea as schlenk:
package require Tclx
for_recursive_glob scriptName $basepath *.tcl {
source $scriptName
}
If you only want folderA and folderB and not other folders under $basepath:
package require Tclx
for_recursive_glob scriptName [list $basepath/folderA $basepath/folderB] *.tcl {
source $scriptName
}

The answer by Joseph Bui works well except that it skips files in the initial folder.
Change:
set directories [list]
To:
set directories [list $directory]
to fix

Related

Wildcard Search with tcl glob

I am trying to search for directories within sub-directories and return any directories that match the wildcard glob search.
The folder structure is as outlined below...
Rootdir
-dir01
-dir_match_01-OLD
-dir_match_01
-dir02
-dir_match_02-OLD
-dir_match_02
-dir03
-dir_match_03-OLD
-dir_match_03
-...
I am searching for directories that would reside in dir01, dir02, dir03 and so on.
I am using the following glob call to recursively search through the directories, which seems to be working correctly...
set rootdir "/home/rootdir/"
set searchstring "*-OLD"
foreach dir [glob -nocomplain -dir $rootdir -type d -- *] {
set result [glob -nocomplain -dir $dir -type d -- $searchstring]
puts $result
}
What I am finding is if I don't use a wildcard in the $searchstring and use an exact directory name that exists I receive the output successfully. But if I then use a wildcard to search for all directories ending in *-OLD It successfully finds them put puts them all out on the same line.
/home/rootdir/dir01/directory01-OLD /home/rootdir/dir01/directory02-OLD /home/rootdir/dir01/directory03-OLD
I have tried to separate the entries by using regsub to replace the whitespace with \n but all it does is remove the whitespace...
/home/rootdir/dir01/directory01-OLD/home/rootdir/dir01/directory02-OLD/home/rootdir/dir01/directory03-OLD
Any suggestions in what I am doing wrong would be much appreciated, thanks.
The most obvious part is that glob always returns a list of names. You'd therefore need to do the innermost loop like this:
foreach dir [glob -nocomplain -dir $rootdir -type d -- *] {
foreach result [glob -nocomplain -dir $dir -type d -- $searchstring] {
puts $result
}
}
However, for a fixed depth search, I think you can do it like this:
foreach dir [glob -nocomplain -dir $rootdir -type d -- */$searchstring] {
puts $dir
}
If you need recursive (full directory tree) search, there are utility commands in Tcllib's fileutil package:
package require fileutil
proc myMatcher {pattern filename} {
# Does the filename match the pattern, and is it a directory?
expr {[string match $pattern $filename] && [file isdir $filename]}
}
set rootdir "/home/rootdir/"
set searchstring "*-OLD"
# Note the use of [list] to create a partial command application
# This is a standard Tcl technique; it's one of the things that [list] is designed to do
foreach dir [fileutil::find $rootdir [list myMatcher $searchstring]] {
puts $dir
}

How to copy files within directory rather than a whole directory in TCL?

Because of software upgrade files are saved to a wrong directory and existing script does not work and I need to fix it. Files are saved few folders deeper in the project file. I know I can use this command to copy directories
file copy -force "path_files_to_copy" "path_to_copied_into"
This way though it will copy the folder, the path is pointing to rather than the each individual folder and file within that folder. How do I copy everything within a specified path to a new location rather than just the parent?
EDIT
I solved it, it works although it seems to me that I am using 10x more steps than it's needed to accomplish same goal. Mainly changing "\" produced by tcl to "\" which will count it as character.
# Copy files from this folder
set from $take_from ;# Everything from this folder
set to $bring_to ;# To this folder
set var [glob -dir $from *;]
set wordList [regexp -inline -all -- {\S+} $var] ;# makes a list
for { set i 0 } {$i < [ llength $var ] } { incr i } { ;# Loop through files found
set file_path [ lindex $var $i ]
set count 0
set indx 0
set limit [string length $file_path]
set file_path_f $file_path
set incra 0
while { $count < $limit } {
set t [string index $file_path $count]
if { "$t" == "\\" } {
set temp $count
set indx $temp
set ll [expr $count + $incra]
set file_path_f [string replace $file_path_f $ll $ll "\\\\"]
incr incra
}
incr count
}
# FILES DESTINATION
file copy -force $file_path_f $to
}
Use the glob command to enumerate the entries under the source directory and copy each of them separately:
foreach f [glob -directory $sourceDir -nocomplain *] {
file copy -force $f $targetDir
}

TCL script to replace C file line

I have C file at
C:\SVN\Code\fileio.c
This reads 2 audio files as
tuningFile = fopen("../../simulation/micdata.bin", "rb");
mic1File = fopen("../../simulation/mic1.pcm", "rb");
I need to write TCL script code that will read the C file, and replace these 2 occurrences to
tuningFile = fopen("C:/SVN/simulation/micdata.bin", "rb");
mic1File = fopen("C:/SVN/simulation/mic1.pcm", "rb");
Can anyone give a compact example for something like below:
read file line wise
search for something like tuningFile = fopen(
extracting path from it and change it to absolute path
combine it with *tuningFile = fopen(
replace original line with modified line at same location
Thanks
sedy
The key is that you actually want to replace:
fopen("../../simulation/
with
fopen("C:/SVN/simulation/
That's easily done with string map. The rest of your problem is then just a matter of doing the file I/O, and pretty much any C source file that can be compiled by an ordinary compiler is best processed by loading it all into memory at once:
set filename {C:\SVN\Code\fileio.c}
set mapping [list {fopen("../../simulation/} {fopen("C:/SVN/simulation/}]
# Slurp the file in
set f [open $filename]
set data [read $f]
close $f
# Apply the mapping
set data [string map $mapping $data]
# Make the original a backup
file rename $filename $filename.bak
# Write back with a separate open
set f [open $filename w]
puts -nonewline $f $data
close $f
If you prefer, you can get the filename as an argument using, say, [lindex $argv 0]. The rest of the code doesn't care.
Here's a version that extracts the filename and uses file normalize on it:
set f [open $filename r]
set code [read $f]
close $f
set code [subst -novar -noback [regsub -all {((?:tuningFile|mic1File) = fopen\(")([^"]+)} $code {\1[file normalize "\2"]}]]
Breaking that up,
this command
regsub -all {((?:tuningFile|mic1File) = fopen\(")([^"]+)} $code {\1[file normalize "\2"]}
will find the string tuningFile = fopen("../relative/file (or "mic1file = ...") and replace it with the text
tuningFile = fopen("[file normalize "../relative/file"]
Then we feed that to subst so that embedded commands can be substituted, executing that file normalize command, resulting in the text
tuningFile = fopen("/full/path/to/file
Take 2: handle brackets in C code
$ pwd
/home/jackman/tmp/base/SVN/Code
$ tree ../..
../..
├── SVN
│   └── Code
│   ├── fileio.c
│   └── normalize.tcl
└── simulation
├── mic1.pcm
└── micdata.bin
3 directories, 4 files
$ cat fileio.c
int tuningFix[MAXTUNING];
tuningFile = fopen("../../simulation/micdata.bin", "rb");
mic1File = fopen("../../simulation/mic1.pcm", "rb");
$ cat normalize.tcl
#! tclsh
package require fileutil
set code [fileutil::cat [lindex $argv 0]]
# protect existing brackets
set bracketmap [list \[ \x01 \] \x02]
set code [string map $bracketmap $code]
# normalize filenames
set code [
subst -novar -noback [
regsub -all {((?:tuningFile|mic1File) = fopen\(")([^"]+)} $code {\1[file normalize "\2"]}
]
]
# restore brackets
set code [string map [lreverse $bracketmap] $code]
puts $code
$ tclsh normalize.tcl fileio.c
int tuningFix[MAXTUNING];
tuningFile = fopen("/home/jackman/tmp/base/simulation/micdata.bin", "rb");
mic1File = fopen("/home/jackman/tmp/base/simulation/mic1.pcm", "rb");
package require fileutil
set filename C:/SVN/Code/fileio.c
set mapping [list {fopen("../../simulation/} {fopen("C:/SVN/simulation/}]
proc replace {mapping data} {
string map $mapping $data
}
::fileutil::updateInPlace $filename [list replace $mapping]
Should work too. (Definition of mapping nicked from Donal.) updateInPlace calls the command prefix in its second argument, passes the contents of the file to that command, and updates the file with the result from the command.
This is very nearly the same procedure as in Donal's answer, expressed in higher-level code. If you want a backup copy, do this before calling updateInPlace:
file copy $filename [file rootname $filename].bak
Documentation: fileutil package, list, proc, set, string
based on great help from all users who commented, I was able to do the task as
proc replaceFileTemp {} {
global pth_fileio_orig
# create backup for copy back
set pth_backup [file rootname $pth_fileio_orig].bak
file copy $pth_fileio_orig $pth_backup
#get current file path
set thisFilePth [ dict get [ info frame [ info frame ] ] file ]
# get folder for current file
set thisFileFolderPth [file dirname $thisFilePth]
# set the replacement string/path
set replacementPth [file dirname $thisFileFolderPth]
# obtain original string to be replaced
set origPth "../../simulation/toplevel"
# download package for file manipulation
package require fileutil
set mapping [list $origPth $replacementPth]
proc replace {mapping data} {
string map $mapping $data
}
# replace original string with replacement string for all occurrences in file
::fileutil::updateInPlace $pth_fileio_orig [list replace $mapping]
}
# set the path to toplevel C file
set pth_fileio_orig [file normalize "../../../fileio.c"]
replaceFileTemp

copy files in one location to another and modify the copied file by placing some data at particular location in tcl?

i have to perform following operation..
copy file from one location to another
search a word in the given file
and move the file pointer to beginning of that line
place the data in that location which are copied from other file...
3 files are as follows:
C:\program Files(X86)\Route\*.tcl
C:\Sanity_Automation\Route\*.tcl
C:\Script.tcl
First i need to copy files from Route folder in Program Files to
Sanity_Automation\Route*.tcl
Then i need to search "CloseAllOutputFile keyword in
C:/Sanity_Automation/Route/SystemTest.tcl
once found, move cursor to the beginning of that line where "CloseAllOutputFile " keyword found.
and place data found on script.tcl to that location.
Firstly, that first "file" is actually a pattern. We need to expand that to a list of real filenames. We do that with glob.
# In braces because there are backslashes
set pattern {C:\Program Files(X86)\Route\*.tcl}
# De-fang the backslashes
set pattern [file normalize $pattern]
# Expand
set sourceFilenames [glob $pattern]
Then we want to copy them. We could do this with:
set target {C:\Sanity_Automation\Route\}
file copy {*}$sourceFilenames [file normalize $target]
But really we also want to build up a list of moved files so that we can process them in the next step. So we do this:
set target {C:\Sanity_Automation\Route\}
foreach f $sourceFilenames {
set t [file join $target [file tail $f]]
file copy $f $t
lappend targetFilenames $t
}
OK, now we're going to do the insertion processing. Let's start by getting the data to insert:
set f [open {C:\Script.tcl}]
set insertData [read $f]
close $f
Now, we want to go over each of the files, read them in, find where to do the insertion, actually do the insertion if we find the place, and then write the files back out. (You do text edits by read/modify-in-memory/write rather than trying to modify the file directly. Always.)
# Iterating over the filenames
foreach t $targetFilenames {
# Read in
set f [open $t]
set contents [read $f]
close $f
# Do the search (this is the easiest way!)
if {[regexp -indices -line {^.*CloseAllOutputFile} $contents where]} {
# Found it, so do the insert
set idx [lindex $where 0]
set before [string range $contents 0 [expr {$idx-1}]]
set after [string range $contents $idx end]
set contents $before$insertData$after
# We did the insert, so write back out
set f [open $t "w"]
puts -nonewline $f $contents
close $f
}
}
Normally, I'd do the modify as part of the copy, but we'll do it your way here.
Try this:
set sourceDir [file join / Files(x86) Route]
set destinationDir [file join / Sanity_Automation Route]
# Read the script to be inserted
set insertFnm [file join / Script.tcl]
set fil [open $insertFnm]
set insertData [read $fil]
close $fil
# Loop around all the Tcl scripts in the source directory
foreach inFnm [glob [file join $sourceDir *.tcl]] {
# Determine the name of the output file
set scriptName [file tail $inFnm]
set outFnm [file join $destinationDir $scriptName]
# Open source and destination files, for input and output respectively
set inFil [open $inFnm]
set outFil [open $outFnm w]
while {![eof $inFil]} {
set line [gets $inFil]
if {[string match *CloseAllOutputFile* $line]} {
puts $outFil $insertData
puts $outFil ""; # Ensure there's a newline at the end
# of the insertion
}
puts $outFil $line
}
# Close input and output files
close $inFil
close $outFil
}
It seems to work for me.

Parsing a file with Tcl

I have a file in here which has multiple set statements. However I want to extract the lines of my interest. Can the following code help
set in [open filename r]
seek $in 0 start
while{ [gets $in line ] != -1} {
regexp (line to be extracted)
}
Other solution:
Instead of using gets I prefer using read function to read the whole contents of the file and then process those line by line. So we are in complete control of operation on file by having it as list of lines
set fileName [lindex $argv 0]
catch {set fptr [open $fileName r]} ;
set contents [read -nonewline $fptr] ;#Read the file contents
close $fptr ;#Close the file since it has been read now
set splitCont [split $contents "\n"] ;#Split the files contents on new line
foreach ele $splitCont {
if {[regexp {^set +(\S+) +(.*)} $ele -> name value]} {
puts "The name \"$name\" maps to the value \"$value\""
}
}
How to run this code:
say above code is saved in test.tcl
Then
tclsh test.tcl FileName
FileName is full path of file unless the file is in the same directory where the program is.
First, you don't need to seek to the beginning straight after opening a file for reading; that's where it starts.
Second, the pattern for reading a file is this:
set f [open $filename]
while {[gets $f line] > -1} {
# Process lines
if {[regexp {^set +(\S+) +(.*)} $line -> name value]} {
puts "The name \"$name\" maps to the value \"$value\""
}
}
close $f
OK, that's a very simple RE in the middle there (and for more complicated files you'll need several) but that's the general pattern. Note that, as usual for Tcl, the space after the while command word is important, as is the space between the while expression and the while body. For specific help with what RE to use for particular types of input data, ask further questions here on Stack Overflow.
Yet another solution:
as it looks like the source is a TCL script, create a new safe interpreter using interp which only has the set command exposed (and any others you need), hide all other commands and replace unknown to just skip anything unrecognised. source the input in this interpreter
Here is yet another solution: use the file scanning feature of Tclx. Please look up Tclx for more info. I like this solution for that you can have several scanmatch blocks.
package require Tclx
# Open a file, skip error checking for simplicity
set inputFile [open sample.tcl r]
# Scan the file
set scanHandle [scancontext create]
scanmatch $scanHandle {^\s*set} {
lassign $matchInfo(line) setCmd varName varValue; # parse the line
puts "$varName = $varValue"
}
scanfile $scanHandle $inputFile
close $inputFile
Yet another solution: use the grep command from the fileutil package:
package require fileutil
puts [lindex $argv 0]
set matchedLines [fileutil::grep {^\s*set} [lindex $argv 0]]
foreach line $matchedLines {
# Each line is in format: filename:line, for example
# sample.tcl:set foo bar
set varName [lindex $line 1]
set varValue [lindex $line 2]
puts "$varName = $varValue"
}
I've read your comments so far, and if I understand you correctly your input data file has 6 (or 9, depending which comment) data fields per line, separated by spaces. You want to use a regexp to parse them into 6 (or 9) arrays or lists, one per data field.
If so, I'd try something like this (using lists):
set f [open $filename]
while {[gets $f line] > -1} {
# Process lines
if {[regexp {(\S+) (\S+) (\S+) (\S+) (\S+) (\S+)} $line -> name source drain gate bulk inst]} {
lappend nameL $name
lappend sourceL $source
lappend drainL $drain
lappend gateL $gate
lappend bulkL $bulk
lappend instL $inst
}
}
close $f
Now you should have a set of 6 lists, one per field, with one entry in the list for each item in your input file. To access the i-th name, for example, you grab $nameL[$i].
If (as I suspect) your main goal is to get the parameters of the device whose name is "foo", you'd use a structure like this:
set name "foo"
set i [lsearch $nameL $name]
if {$i != -1} {
set source $sourceL[$i]
} else {
puts "item $name not found."
set source ''
# or set to 0, or whatever "not found" marker you like
}
set File [ open $fileName r ]
while { [ gets $File line ] >= 0 } {
regex {(set) ([a-zA-Z0-0]+) (.*)} $line str1 str2 str3 str4
#str2 contains "set";
#str3 contains variable to be set;
#str4 contains the value to be set;
close $File
}