Copy multiple files in TCL 8.4 - tcl

I saw multiple answers to this question but none of them were specific to TCL 8.4 or prior.
So here's the code that I tried but didn't work:
set files [ glob home/*.tcl]
file copy {*}$files dest/
Copying issue is sorted by using eval as one of the comments suggests.
Moving the file should be done with:
eval file rename -force $flist dest/

Referring to the 8.5+ solution from How to copy or move multiple files with same extension?: There is no expansion operator prior to 8.5.
The example would become:
set files [glob home/*.tcl]
eval file copy $files dest/
That said, if possible, you should upgrade to 8.5+ (for a number of reasons), but avoiding [eval] in such expansion scenarios is advised. Alternativey, you might want to expand by looping explicitly through $files, but this has also a performance downside.

Related

How does Tcl "file rename {*}[glob *tcl] dir/ " operate

I am trying to move a large number of files using Tcl and came across the expression :
file rename {*}[glob *tcl] dir/ which works perfectly.
Can anyone explain how this command works or what this feature is called?
It's a compound of two commands and some useful syntax.
glob returns a list of filenames that match the pattern, *tcl in your case, or an error if nothing matches. There's a bunch of options you could use to modify what it returns, but you're not using any of them; that's great for your use case.
file rename will rename files or move files around. In particular, when the final argument is an existing directory name, the other arguments are files (or directories) that will be moved into that directory. (That it moves things around is sensible if you're familiar with how POSIX system calls work.)
The final piece of the puzzle is {*}[…], i.e., command expansion, which runs a command (which is glob *tcl in your case) and uses the elements of the list it returns as a sequence of arguments to the command call within which it is used. Which is useful; we want a list of filenames at that point of the call to file rename. There's no real limit on the number of arguments that can be moved around that way, other than basic things like memory and so on.
The {*} prefix (it's only special at the start of a word) can be used with other well-formed ways of producing a Tcl word (e.g., a read from a variable with $ or a literal with {…}) or even with a compound word, though use with compound words is usually a sign that what you're doing is probably unwise.
If you have old Tcl code, written for Tcl 8.4 or before, you won't see {*}. Instead, you'd see something like this:
eval file rename [glob *tcl] dir/
# Or, more properly, one of these horrors:
eval {file rename} [glob *tcl] {dir/}
eval [list file rename] [glob *tcl] [list dir/]
eval [linsert [linsert [glob *tcl] 0 file rename] end dir/]
These were notoriously awkward to get right in tricky cases (causing many subtle bugs). The expansion syntax was added in Tcl 8.5 exactly to get rid of this whole class of trouble. eval still exists in modern Tcl, but it is now thankfully rarely used.

How to copy or move multiple files with same extension?

So I am trying to move a bunch of files with similar extensions from /home/ to /root/
Code I tried is
file copy /home/*.abc.xyz /root/
Also tried
set infile [glob -nocomplain /home/*.abc.xyz ]
if { [llength $infile] > 0 } {
file copy $infile /root/
}
No success.
Your two attempts fail for different reasons:
There is no wildcard expansion in arguments to file copy, or any Tcl command, for that matter: file copy /home/*.abc.xyz /root/. This will look for a single source with a literal * in its filename.
glob -nocomplain /home/*.abc.xyz is ok to collect the sources, but glob returns a list of sources. file copy requires each source to passed as a separate argument, not a single one. To expand a single collection value of source files into a multiple separate arguments, use the Tcl expansion operator {*}
Therefore:
set infiles [glob -nocomplain *.tcl]
if {[llength $infiles]} {
file copy {*}$infiles /tmp/tgt/
}
For a 1-line answer:
file copy {*}[glob /home/*.abc.xyz] /root/.
The file copy (and file rename) commands have two forms (hence the reference to the manual page in the comment). The first form copies a single file to a new target. The second form copies all the file name arguments to a new directory and this form of the command insists that the directory name be the last argument and you may have an arbitrary number of source file names preceding. Also, file copy does not do glob expansion on its arguments, so as you rightly surmised, you also need to use the glob command to obtain a list of the files to copy. The problem is that the glob command returns a list of file names and you passed that list as a single argument, i.e.
file copy $infile /root/
passes the list as a single argument and so the file copy command thinks it is dealing with the first form and attempts to find a file whose name matches that of the entire list. This file probably doesn't exist. Placing the error message in your question would have helped us to know for sure.
So what you want to do is take the list of files contained in the infile variable and expand it into separate argument words. Since this is a common situation, Tcl has some syntax to help (assuming you are not using some ancient version of Tcl). Try using the command:
file copy {*}$infile /root/
in place of your first attempt and see if that helps the situation.

How to search a a directory tree in TCL for files that end in .so or .a and build a list of those directories

I'm trying to write a set of TCL scripts that helps setup a user's environment for a set of libraries that are not in their standard LD_LIBRARY_PATH as part of a support solution.
Since the system is rather sparse in terms of what I can install, I don't really have access to any TCL extensions and am trying to do this in base TCL as much as possible. I'm also relatively new to TCL as a language.
What I'd like to do is search through a directory structure and locate the directories that have .so and .a files in them, build a list of those, and, eventually add them to the user's $LD_LIBRARY_PATH variable.
If I were doing this in a shell, I'd just use find with something like this:
find /dir -type f \( -name "*.so" -o -name "*.a" \) | awk -F/ 'sub(FS $NF,x)' | sort -u
I could hard-code the paths, but we want a single set of scripts that can manage several different applications.
Any ideas would be very much appreciated.
Tcllib has a fileutil module that does a recursive find:
package require fileutil
set filenames [::fileutil::findByPattern /dir -glob {*.so *.a}]
foreach filename $filenames {
if {[file isfile $filename]} {
set dirs([file dirname $filename]) 1
}
}
puts [array names dirs]
If you want to use this, but can't install something, you can just take the procedures and add them to your code (with the appropriate attribution) -- http://core.tcl.tk/tcllib/dir?ci=63d99a74f4600441&name=modules/fileutil
Otherwise, just call the system's find command and parse the output (assume that your filenames do not contain newlines).
set filenames [split [exec find /dir -type f ( -name *.so -o -name *.a )] \n]
And the loop to extract the unique directories is similar. As a side benefit, the find invocation is actually easier to read because you don't have to escape all the chars that are special to the shell.

How to copy multiple files with Tcl file command

From Tcl online manual I see that Tcl's file copy command can take multiple source files as argument:
file copy ?-force? ?--? source ?source ...? targetDir
However, I have the following code:
set flist [list a.txt b.txt]
file copy $flist [file join D:\\ test dest]
And get this error message:
error copying "a.txt b.txt": no such file or directory
How do I properly pass a file list as source argument to the file copy command?
The right way to do this is to use expansion:
file copy {*}$flist {D:\test\dest}
The {*} substitutes the words of the list given by what follows it as separate words; it's precisely right here.
I've also written the destination directory as a brace-quoted literal.
Still on Tcl 8.4 or before? Upgrade! Or use this:
eval file copy $flist [list {D:\test\dest}]
It's quite a lot harder to use eval right than {*}, so really upgrade.
Or even do:
foreach f $flist {
file copy $f {D:\test\dest}
}
Given that IO operations will dominate the performance, you shouldn't notice any speed difference for doing it this way.
The problem is the list is passed as a whole to the command instead of individual elements. Use {*} operator to break the list down to its individual elements.
The short answer is don't use a list the way you have done.
This works in your example:
set flist "a.txt b.txt"
file copy $flist [file join D:\\ test dest]
More correct would be to use the list expansion {*} syntax.

TCL- script to output a file which contains size of all the files in the directry and subdirectories

Please help me with the script which outputs the file that contains names of the files in subdirectories and its memory in bytes, the arguement to the program is the folder path .output file should be file name in 1st column and its memory in second column
Note:folder contains subfolders...inside subfolders there are files
.I tried this way
set fp [open files_memory.txt w]
set file_names [glob ../design_data/*/*]
foreach file $file_names {
puts $fp "$file [lindex [exec du -sh $file] 0]"
}
close $fp
Result sample:
../design_data/def/ip2.def.gz 170M
../design_data/lef/tsmc13_10_5d.lef 7.1M
But i want only file name to be printed that is ip2.def.gz , tsmc13_10_5d.lef ..etc(not the entirepath) and file memorry should be aligned
TCL
The fileutil package in Tcllib defines the command fileutil::find, which can recursively list the contents of a directory. You can then use foreach to iterate over the list and get the sizes of each of them with file size, before producing the output with puts, perhaps like this:
puts "$filename\t$size"
The $filename is the name of the file, and the $size is how large it is. You will have obtained these values earlier (i.e., in the line or two before!). The \t in the middle is turned into a TAB character. Replace with spaces or a comma or virtually anything else you like; your call.
To get just the last part of the filename, I'd do:
puts $fp "[file tail $file] [file size $file]"
This does stuff with the full information about the file size, not the abbreviated form, so if you really want 4k instead of 4096, keep using that (slow) incantation with exec du. (If the consumer is a program, or a programmer, writing out the size in full is probably better.)
In addition to Donal's suggestion, there are more tools for getting files recursively:
recursive_glob (from the Tclx package) and
for_recursive_glob (also from Tclx)
fileutil::findByPattern (from the fileutil package)
Here is an example of how to use for_recursive_glob:
package require Tclx
for_recursive_glob filename {../design_data} {*} {
puts $filename
}
This suggestion, in combination with Donal's should be enough for you to create a complete solution. Good luck.
Discussion
The for_recursive_glob command takes 4 arguments:
The name of the variable representing the complete path name
A list of directory to search for (e.g. {/dir1 /dir2 /dir3})
A list of patterns to search for (e.g. {*.txt *.c *.cpp})
Finally, the body of the for loop, where you want to do something with the filename.
Based on my experience, for_recursive_glob cannot handle directories that you don't have permission to (i.e. on Mac, Linux, and BSD platforms, I don't know about Windows). In which case, the script will crash unless you catch the exception.
The recursive_glob command is similar, but it returns a list of filenames instead of structuring in a for loop.