How to copy multiple files with Tcl file command - tcl

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.

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.

Copy multiple files in TCL 8.4

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.

Converting Tcl to C++

I am trying to convert some tcl script into a C++ program. I don't have much experience with tcl and am hoping someone could explain what some of the following things are actually doing in the tcl script:
1) set rtn [true_test_sfm $run_dir]
2) cd [glob $run_dir]
3) set pwd [pwd]
Is the first one just checking if true_test_sfm directory exists in run_dir?
Also, I am programming on a windows machine. Would the system function be the equivalent to exec statements in tcl? And if so how would I print the result of the system function call to stdout?
In Tcl, square brackets indicate "evaluate the code between the square brackets". The result of that evaluation is substituted for the entire square-bracketed expression. So, the first line invokes the function true_test_sfm with a single argument $run_dir; the result of that function call is then assigned to the variable rtn. Unfortunately, true_test_sfm is not a built-in Tcl function, which means it's user-defined, which means there's no way we can tell you what the effect of that function call will be based on the information you've provided here.
glob is a built-in Tcl function which takes a file pattern as an argument and then lists files that match that pattern. For example, if a directory contains files "foo", "bar" and "baz", glob b* would return a list of two files, "bar" and "baz". Therefore the second line is looking for any files that match the pattern given by $run_dir, then using the cd command (another Tcl built-in) to change to the directory found by glob. Probably $run_dir is not actually a file pattern, but an explicit file name (ie, no globbing characters like * or ? in the string), otherwise this code may break unexpectedly. On Windows, some combination of FindFirstFile/FindNextFile in C++ could be used as a substitute for glob in Tcl, and SetCurrentDirectory could substitute for cd.
pwd is another built-in Tcl function which returns the process current working directory as an absolute path. So the last line is querying the current working directory and saving the result in a variable named pwd. Here you could use GetCurrentDirectory as a substitute for pwd.

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.