Exec a subprocess : pass PYTHONPATH to the subprocess - tcl

When I am executing a Python script and ensure that PYTHONPATH is properly set to refer depedency modules. Within the Python code I call a TCL script which again calls a Python script like below:
if {[catch {exec {*}[auto_execok python] [file nativename [file join [file dirname [info script]] my.py ]] } result] == 0 } {
puts "Executed successfully $result"
} else {
puts "Error $result"
return error
}
I am successfully able to execute the Python script my.py externally but when executed from the TCL script it gives issues. Somehow I find that it is cause the PYTHONPATH is not being passed properly while calling the Python script since my.py refers to depdency Python modules.
How can I pass the PYTHONPATH in exec command?

The PYTHONPATH is an environment variable. They're manipulated through the env global variable:
# You might be able to set this once for your whole script
set python_path {C:/Python/3.6/wherever C:/Users/me/Python/3.6/wherever}
# Transform a Tcl list into the right format that Python expects
set ::env(PYTHONPATH) [join [lmap p $python_path {file nativename $p}] \
$::tcl_platform(pathSeparator)]
# Split this out for a shorter line length. ;-)
set my_py [file join [file dirname [info script]] my.py]
if {[catch {exec {*}[auto_execok python] [file nativename $my_py]} result] == 0 } {
puts "Executed successfully $result"
} else {
puts "Error $result"
return error
}
In Tcl 8.5, you don't have lmap or the pathSeparator element of tcl_platform and instead would do something like this:
foreach p $python_path {
if {[info exist ::env(PYTHONPATH)]} {
# Assume Windows
append ::env(PYTHONPATH) ";" [file nativename $p]
} else {
set ::env(PYTHONPATH) [file nativename $p]
}
}
You could also hardcode the values if they're just one or two elements. Remember that backslashes (\) are significant to Tcl, so put the string in {…} if you're doing that.
set ::env(PYTHONPATH) {C:\Python\3.6\wherever;C:\Users\me\Python\3.6\wherever}
That's not particularly viable for anything redistributable… but works for one's own scripts.

Related

Is it possible to capture the error output of glob?

Suppose if I have the following glob command wrapped in try/trap/finally:
proc generateSubmissionFolder {cover_letter_resume submission_path} {
set submission_parent [file dirname $submission_path]
set latest_submission_folder [lindex [lsort [glob -directory $submission_parent -type d *]] end]
set latest_submission_file [lindex [glob -directory $latest_submission_folder *[file extension $cover_letter_resume]] end]
createSubmissionFolder $latest_submission_file $submission_path
}
proc createSubmissionFolder {source destination} {
puts "Creating $destination folder."
file mkdir $destination
puts "Copying $source to $destination"
file copy $source $destination
}
try {
# I gathered user input and stored them in the variables $company_name and $position.
set submission_path [file join $company_name $position $yearmonthday]
if {[file exists [file dirname $submission_path]]} {
generateSubmissionFolder $coverletterresume $submission_path
} else {
createSubmissionFolder $coverletterresume $submission_path
}
} trap {Value Empty} {errormessage} {
puts "$errormessage"
} finally {
puts "$argv0 exiting."
}
If no folder is found, I would like to provide a human readable error message, but I'm unsure of what error to trap. According to the answer to my previous question:
Tcl doesn't have a pre-defined hierarchy of exceptions.
The only work around I tried was to use the -nocomplain switch and afterward check if the latest_submission_folder was blank.
Is there a way to trap a FileNotFound or FolderNotFound error?
For trivial cases like your example, use an on error handler, not trap. Or use catch instead of try.
Example tclsh session:
% try { glob *.bar } on error {what} { puts "Ooops: $what" }
Ooops: no files matched glob pattern "*.bar"
% if {[catch { glob *.bar } result] == 1} { puts "Ooops: $result" }
Ooops: no files matched glob pattern "*.bar"
Or if you do want to use trap because you also want to handle a bunch of other possible specific errors from more complicated code, glob raises a TCL OPERATION GLOB NOMATCH on failure:
% try { glob *.bar } trap {TCL OPERATION GLOB NOMATCH} {msg} { puts "Ooops: $msg" }
Ooops: no files matched glob pattern "*.bar"
You can discover what to use with trap for any given command's particular error with something like:
% catch { glob *.bar } result errdict
1
% dict get $errdict -errorcode
TCL OPERATION GLOB NOMATCH
In this specific case, glob has an option that helps: -nocomplain. It turns off the error on no match — which was only ever really intended for interactive use — as a lot of use cases can cope with an empty returned list just fine. (It's the way it is for historical reasons, and is maintained that way so we don't break the large number of existing scripts that use it. As language warts go, it's not too terrible.)
set latest_submission_folder [glob -directory $submission_parent -type d *]
if {![llength $latest_submission_folder]} {
# Make your own handling right here. Whatever you want.
puts "I didn't find a directory inside $submission_parent"
} elseif {[llength $latest_submission_folder] > 1} {
# Many subdirectories found. Is this an error given your single-thing var name?
} else {
# Strip a layer of list; good idea in case directory name has a space in it!
set latest_submission_folder [lindex $latest_submission_folder 0]
}

How to prevent tcl script from exiting?

I am running tclsh some.tcl and it exits after it hits eof. I want it not to exit and gives control to user for interaction. Note that we can do this by invoking shell and sourcing script but that doesn't solve my problem as it cannot be used in automation.
If you can load the TclX package (old but still useful) then you can do:
package require Tclx; # Lower case at the end for historical reasons
# Your stuff here
commandloop
That's very much like how Tcl's own interactive command line works.
Otherwise, here's a scripted version that does most of what an interactive command session does:
if {![info exists tcl_prompt1]} {
set tcl_prompt1 {puts -nonewline "% ";flush stdout}
}
if {![info exists tcl_prompt2]} {
# Note that tclsh actually defaults to not printing anything for this prompt
set tcl_prompt2 {puts -nonewline "> ";flush stdout}
}
set script ""
set prompt $tcl_prompt1
while {![eof stdin]} {
eval $prompt; # Print the prompt by running its script
if {[gets stdin line] >= 0} {
append script $line "\n"; # The newline is important
if {[info complete $script]} { # Magic! Parse for syntactic completeness
if {[catch $script msg]} { # Evaluates the script and catches the result
puts stderr $msg
} elseif {$msg ne ""} { # Don't print empty results
puts stdout $msg
}
# Accumulate the next command
set script ""
set prompt $tcl_prompt1
} else {
# We have a continuation line
set prompt $tcl_prompt2
}
}
}
Getting the remaining bits right (e.g., the interaction with the event loop when the Tk package is loaded) would require quite a bit more complexity...

Is there is a way of sourcing csh file in tcl script and bringing variables to the main shell?

I'm trying to source a some.cshrc during TCL script execution.
I get no error but the variables set in some.cshrc are not passed back to the shell.
When writing it like:
source some.cshrc
I'm getting an error.
Then I tried:
exec /bin/csh -c "source some.cshrc"
Please advise
Since Csh and Tcl are two completely different languages, you cannot simply load a Csh file in a Tcl script via a source command.
You can think of a workaround instead. Let us assume you want to load all the variables set with a setenv command. Example contents of some.cshrc file could look something like this:
setenv EDITOR vim
setenv TIME_STYLE long-iso
setenv TZ /usr/share/zoneinfo/Europe/Warsaw
You can write a Tcl script which reads this file line by line and searches for setenv commands. You can then reinterpret the line as a list and set an appropriate variable in a global namespace via an upvar command.
#!/usr/bin/tclsh
proc load_cshrc {file_name} {
set f [open $file_name r]
while {[gets $f l] >= 0} {
if {[llength $l] == 3 && [lindex $l 0] eq "setenv"} {
upvar [lindex $l 1] [lindex $l 1]
set [lindex $l 1] [lindex $l 2]
}
}
close $f
}
load_cshrc "some.cshrc"
puts "EDITOR = $EDITOR"
puts "TIME_STYLE = $TIME_STYLE"
puts "TZ = $TZ"
Please note, that since we do not run a Csh subshell any variable substitutions in the Csh script would not occur.

How to run executable within the tcl?

I am trying to execute program which has some options, and take as an input txt file. So I have try this:
set myExecutable [file join $::env(path_to_the_program) bin executable_name]
if { ![file exists $myExecutable ] } {
puts "error"
}
if { ![file executable $myExecutable ] } {
puts "error"
}
set arguments [list -option1 -option2]
set status [catch { exec $myExecutable $arguments $txtFileName } output]
if { $status != 0 } {
puts "output = $output"
}
So it's print:
output = Usage: executable_name -option1 -option2 <txt_file_name>
child process exited abnormally
You didn't actually provide the arguments to you executable. Just the textFileName. Try:
set status [catch {exec $myExecutable -option1 -option2 $txtFileName} output]
or if you prefer to keep the arguments in a list:
set status [catch {exec $myExecutable {*}$arguments} output]
where the {*} syntax will cause the list to be expanded in place. In Tcl versions before this was added (8.5) you would use:
set status [catch {eval exec [list $myExecutable] $arguments} output]
where the eval command unwraps the lists so that exec sees a single set of arguments. Adding the extra [list] statement around your $myExecutable protects it's contents against being treated as a list by the interpreter pass.

TCL: Check file existence by SHELL environment variable

I have a shell environment variable PATH_TO_DIR and I want to check in TCL script that the file $PATH_TO_DIR/target.txt exists.
My current solution is:
catch {exec /usr/local/bin/tcsh -c "echo $PATH_TO_DIR/target.txt" } result
if {![file exists $result]} {
puts "ERROR: the file $result is not exists"
}
I'm sure there is a more elegant way.
How can I solve it only with TCL commands?
set path_to_dir $::env(PATH_TO_DIR)
set file_name [file join $path_to_dir "target.txt"]
set native_file_name [file nativename $file_name]
if {![file exists $native_file_name]} {
puts "ERROR: the file $native_file_name does not exist"
}