diff --git a/src/punk86.vfs/lib/app-punk/repl.tcl b/src/punk86.vfs/lib/app-punk/repl.tcl index dbdaef5..ac81940 100644 --- a/src/punk86.vfs/lib/app-punk/repl.tcl +++ b/src/punk86.vfs/lib/app-punk/repl.tcl @@ -1,90 +1,96 @@ -package provide app-punk 1.0 - - -#punk linerepl launcher - - - -#------------------------------------------------------------------------------ -#Module loading -#------------------------------------------------------------------------------ -#If the current directory contains .tm files when the punk repl starts - then it will attempt to preference them -# - but first add our other known relative modules paths - as it won't make sense to use current directory as a modulpath if it's an ancestor of one of these.. - -set original_tm_list [tcl::tm::list] -tcl::tm::remove {*}$original_tm_list - - -#1) -if {[string match "*.vfs/*" [info script]]} { - #src/xxx.vfs/lib/app-punk/repl.tcl - #we assume if calling directly into .vfs that the user would prefer to use src/modules - so go up 4 levels - set modulefolder [file dirname [file dirname [file dirname [file dirname [info script]]]]]/modules - -} else { - # .../bin/punkXX.exe look for ../modules (i.e modules folder at same level as bin folder) - set modulefolder [file dirname [file dirname [info nameofexecutable]]]/modules -} - -if {[file exists $modulefolder]} { - tcl::tm::add $modulefolder -} else { - puts stderr "Warning unable to find module folder at: $modulefolder" -} -if {[file exists [pwd]/modules]} { - catch {tcl::tm::add [pwd]/modules} -} - - -#2) -#now add current dir (if no conflict with above) -set currentdir_modules [glob -nocomplain -dir [pwd] -type f -tail *.tm] -set tcl_core_packages [list tcl::zlib zlib tcl::oo TclOO tcl::tommath tcl::zipfs Tcl Tk] -if {[llength $currentdir_modules]} { - #only forget all *unloaded* package names if we are started in a .tm containing folder - foreach pkg [package names] { - if {$pkg in $tcl_core_packages} { - continue - } - if {![llength [package versions $pkg]]} { - #puts stderr "Got no versions for pkg $pkg" - continue - } - if {![string length [package provide $pkg]]} { - package forget $pkg - } - } - catch {tcl::tm::add [pwd]} -} - - - -package require Thread -#These are strong dependencies -# - the repl requires Threading and punk,shellfilter,shellrun to call and display properly. -# tm list already indexed - need 'package forget' to find modules based on current tcl::tm::list - -package forget shellfilter -package require shellfilter -package forget shellrun -package require shellrun -package forget punk -package require punk - - -#restore module paths -set tm_list_now [tcl::tm::list] -foreach p $original_tm_list { - if {$p ni $tm_list_now} { - #the prior tm paths go to the head of the list. - #They are processed first.. but an item of same version later in the list will override one at the head. - tcl::tm::add $p - } -} -#------------------------------------------------------------------------------ - -package require punk::repl -repl::start stdin - - - +package provide app-punk 1.0 + + +#punk linerepl launcher + + + +#------------------------------------------------------------------------------ +#Module loading +#------------------------------------------------------------------------------ +#If the current directory contains .tm files when the punk repl starts - then it will attempt to preference them +# - but first add our other known relative modules paths - as it won't make sense to use current directory as a modulpath if it's an ancestor of one of these.. + +set original_tm_list [tcl::tm::list] +tcl::tm::remove {*}$original_tm_list + + +#1) +if {[string match "*.vfs/*" [info script]]} { + #src/xxx.vfs/lib/app-punk/repl.tcl + #we assume if calling directly into .vfs that the user would prefer to use src/modules - so go up 4 levels + set modulefolder [file dirname [file dirname [file dirname [file dirname [info script]]]]]/modules + +} else { + # .../bin/punkXX.exe look for ../modules (i.e modules folder at same level as bin folder) + set modulefolder [file dirname [file dirname [info nameofexecutable]]]/modules +} + +if {[file exists $modulefolder]} { + tcl::tm::add $modulefolder +} else { + puts stderr "Warning unable to find module folder at: $modulefolder" +} +if {[file exists [pwd]/modules]} { + catch {tcl::tm::add [pwd]/modules} +} +if {[file exists [pwd]/lib]} { + lappend ::auto_path [pwd]/lib +} +set libfolder [file dirname [file dirname [info nameofexecutable]]]/lib +if {[file exists $libfolder]} { + lappend ::auto_path $libfolder +} + +#2) +#now add current dir (if no conflict with above) +set currentdir_modules [glob -nocomplain -dir [pwd] -type f -tail *.tm] +set tcl_core_packages [list tcl::zlib zlib tcl::oo TclOO tcl::tommath tcl::zipfs Tcl Tk] +if {[llength $currentdir_modules]} { + #only forget all *unloaded* package names if we are started in a .tm containing folder + foreach pkg [package names] { + if {$pkg in $tcl_core_packages} { + continue + } + if {![llength [package versions $pkg]]} { + #puts stderr "Got no versions for pkg $pkg" + continue + } + if {![string length [package provide $pkg]]} { + package forget $pkg + } + } + catch {tcl::tm::add [pwd]} +} + + +#puts stdout "$::auto_path" +package require Thread +#These are strong dependencies +# - the repl requires Threading and punk,shellfilter,shellrun to call and display properly. +# tm list already indexed - need 'package forget' to find modules based on current tcl::tm::list + +package forget shellfilter +package require shellfilter +package forget shellrun +package require shellrun +package forget punk +package require punk + + +#restore module paths +set tm_list_now [tcl::tm::list] +foreach p $original_tm_list { + if {$p ni $tm_list_now} { + #the prior tm paths go to the head of the list. + #They are processed first.. but an item of same version later in the list will override one at the head. + tcl::tm::add $p + } +} +#------------------------------------------------------------------------------ + +package require punk::repl +repl::start stdin + + + diff --git a/src/punk86.vfs/lib/app-shellspy/shellspy.tcl b/src/punk86.vfs/lib/app-shellspy/shellspy.tcl index 4c04b63..33d2dfb 100644 --- a/src/punk86.vfs/lib/app-shellspy/shellspy.tcl +++ b/src/punk86.vfs/lib/app-shellspy/shellspy.tcl @@ -1,848 +1,851 @@ -#! /usr/bin/env tclsh -# -#copyright 2023 Julian Marcel Noble -#license: BSD (revised 3-clause) -# -#see notes at beginning of shellspy namespace re stdout/stderr -# -#SHELLSPY - provides commandline flag information and stdout/stderr logging/mirroring without output/pipeline interference, -# or modified output if modifying filters explicitly configured. -# -#shellspy uses the shellfilter and flagfilter libraries to run a commandline with tee-diversions of stdout/stderr to configured syslog/file logs -#Because it is a tee, the command's stdout/stderr are still available as direct output from this script. -#Various filters/modifiers/redirections can be placed on the channel stacks for stdin/stderr using the shellfilter::stack api -# and other shellfilter:: helpers such as shellfilter::redir_output_to_log -# Redirecting stderr/stdout in this way prior to the actual command run will not interfere with the command/pipeline output due to the way -# shellfilter temporarily inserts it's own tee into the stack at the point of the highest existing redirection it encounters. -# -#A note on input/output convention regarding channels/pipes -# we write to an output, read from an input. -# e.g when creating a pipe with 'lassign [chan pipe] in out' we write to out and read from in. -# This is potentially confusing from an OO-like perspective thinking of a pipe object as a sort of object. -# Don't think of it from the perspective of the pipe - but from the program using it. -# This is not a convention invented here for shellspy - but it seems to match documentation and common use e.g for 'chan pending input|output chanid' -# This matches the way we write to stdout read from stdin. -# Possibly using read/write related naming for pipe ends is clearer. eg 'lassign [chan pipe] rd_pipe1 wr_pipe1' -# -package provide app-shellspy 1.0 - - -#a test for windows -#fconfigure stdin -encoding utf-16le -#fconfigure stdout -encoding utf-16le - -#tcl::tm::remove {*}[tcl::tm::list] -set original_tm_list [tcl::tm::list] -tcl::tm::remove {*}$original_tm_list - -if {[string match "*.vfs/*" [info script]]} { - #src/xxx.vfs/lib/app-punk/repl.tcl - #we assume if calling directly into .vfs that the user would prefer to use src/modules - so go up 4 levels - set m_dir [file dirname [file dirname [file dirname [file dirname [info script]]]]]/modules -} else { - #add dir outside of tclkit/exe so we can override with higher versions if necessary without rebuilding - # .../bin/punkXX.exe look for ../modules (i.e modules folder at same level as bin folder) - set m_dir [file normalize [file join [file dirname [file dirname [info nameofexecutable]]] modules]] -} -tcl::tm::add $m_dir -#set m_dir [file normalize [file join [file dirname [info script]] ../../../modules]] -#tcl::tm::add $m_dir - -#experiment - todo make a flag for it if it's useful -#Middle cap for direct dispatch without flagcheck arg processing or redirections or REPL. -set arg1 [lindex $::argv 0] -if {[file extension $arg1] in [list .tCl]} { - set ::argv [lrange $::argv 1 end] - set ::argc [llength $::argv] - - set exedir [file dirname [info nameofexecutable]] - set libroot [file join $exedir scriptlib] - set scriptname $arg1 - if {[string match lib::* $scriptname]} { - set scriptname [string map [list "lib::" "" "::" "/"] $scriptname] - set scriptpath $libroot/$scriptname - } else { - set scriptpath [file normalize $scriptname] - } - - if {![file exists $scriptpath]} { - #try the lowercase version (extension lowercased only) so that file doesn't have to be renamed to use alternate dispatch - set scriptpath [file rootname $scriptpath][string tolower [file extension $scriptpath]] - } - source $scriptpath - - #package require app-punk - -} else { - - - -#set m_dir [file join $starkit::topdir modules] - -#lappend auto_path c:/tcl/lib/tcllib1.20 - -package require Thread - -#NOTE: tm package index will probably already have been created so we must use 'package forget' to restrict to current tcl::tm::list path -#Review - effect on load time of wasting a previously created index? better way? -#require core modules only from punk distribution (REVIEW - override option?) -package forget flagfilter -package require flagfilter -package forget shellfilter -package require shellfilter -package forget punk -package require punk - -#restore module paths -set tm_list_now [tcl::tm::list] -foreach p $original_tm_list { - if {$p ni $tm_list_now} { - tcl::tm::add $p - } -} - - -#package require packageTrace - -set ::testconfig 5 - -namespace eval shellspy { - variable shellspy_status_log "shellspy-[clock micros]" - #shellfilter::log::open $shellspy_status_log [list -tag $shellspy_status_log -syslog 172.16.6.42:51500 -file ""] - shellfilter::log::open $shellspy_status_log [list -tag $shellspy_status_log -syslog 127.0.0.1:514 -file ""] - shellfilter::log::write $shellspy_status_log "shellspy launch with args '$::argv'" - shellfilter::log::write $shellspy_status_log "stdout/stderr encoding: [chan configure stdout -encoding]/[chan configure stderr -encoding]" - - #------------------------------------------------------------------------- - ##don't write to stdout/stderr before you've redirected them to a log using shellfilter functions - ## puts to stdout/stderr will comingle with command's output if performed before the channel stacks are configured. - - chan configure stdin -buffering line - chan configure stdout -buffering none - chan configure stderr -buffering none - - #set id_ansistrip [shellfilter::stack::add stderr ansistrip -settings {}] - #set id_ansistrip [shellfilter::stack::add stdout ansistrip -settings {}] - - lassign [shellfilter::redir_output_to_log "SPY"] id_stdout_redir id_stderr_redir - - - ### - #we can now safely write to stderr/stdout and it will not interfere with stderr/stdout from the dispatched command. - #This is because when shellfilter::run is called it temporarily adds it's own filter in place of the redirection we just added. - # shellfilter::run installs tee_to_pipe on stdout & stderr with -action sink-aside. - # sink-aside means to sink down the filter stack to the topmost filter that is diversionary, and replace it. - # when the command finishes running, the redirecting filter that it displaced is reinstalled in the stack. - ### - - ### - #Note that futher filters installed here will sit 'above' any of the redirecting filters - # so apply to both the shellfilter::run commandline, - # as well as writes to stderr/stdout from here or other libraries operating in this process. - # To bypass the the filter-stack and still emit to syslog etc - - # you can use shellfilter::log::open and shellfilter::log::write e.g - # shellfilter::log::open "mystatuslog" [list -tag "mystatuslog" -syslog 172.16.6.42:51500 -file ""] - # shellfilter::log::write "mystatuslog" "shellspy launch" - # - #### - #set id_ansistrip [shellfilter::stack::add stderr ansistrip -action float -settings {}] - #set id_ansistrip [shellfilter::stack::add stdout ansistrip -action float -settings {}] - - - ##stdin stack operates in reverse compared to stdout/stderr in that first added to stack is first to see the data - ##for stdin: first added to stack is 'upstream' as it will always encounter the data before later ones added to the stack. - ##for stdout: first added to stack is 'downstream' as it will alays encounter the data after others on the stack - #shellfilter::stack::add stdin ansistrip -action {} -settings {} - #shellfilter::stack::add stdin tee_to_log -settings {-tag "input" -raw 1 -syslog 172.16.6.42:51500 -file ""} - - #------------------------------------------------------------------------- - ##note - to use shellfilter::stack::new - the fifo2 end at the local side requires the event loop to be running - ## for interactive testing a relatively simple repl.tcl can be used. - - #todo - default to no logging.. add special flag in checkflags to indicate end of options for matched command only e.g --- ? - # then prioritize these results from checkflags to configure pre-dispatch behaviour by running a predispatch script(?) - # - # we can log flag-processing info coming from checkflags.. but we don't want to do two opt scans and emit it twice. - # configuration of the logging for flag/opt parsing should come from a config file and default to none. - set stdout_log [file normalize ~]/shellspy-stdout.txt - set stderr_log [file normalize ~]/shellspy-stderr.txt - - set outdeviceinfo [shellfilter::stack::new shellspyout -settings [list -tag "shellspyout" -buffering none -raw 1 -syslog 127.0.0.1:514 -file $stdout_log]] - set commandlog [dict get $outdeviceinfo localchan] - #puts $commandlog "HELLO $commandlog" - #flush $commandlog - set errdeviceinfo [shellfilter::stack::new shellspyerr -settings [list -tag "shellspyerr" -buffering none -raw 1 -syslog 127.0.0.1:514 -file $stderr_log]] - - #note that this filter is inline with the data teed off to the shellspyout log. - #To filter the main stdout an addition to the stdout stack can be made. specify -action float to have it affect both stdout and the tee'd off data. - set id_ansistrip [shellfilter::stack::add shellspyout ansistrip -settings {}] - - - #set id_out [shellfilter::stack::add stdout rebuffer -settings {}] - - #an example filter to capture some output to a var for later use - this one is for ansible-playbook - #set ::recap "" - #set id_tee_grep [shellfilter::stack::add shellspyout tee_grep_to_var -settings {-grep ".*PLAY RECAP.*|.*fatal:.*|.*changed:.*" -varname ::recap -pre 1 -post 3}] - - namespace import ::flagfilter::check_flags - - namespace eval shellspy::callbacks {} - namespace eval shellspy::parameters {} - - - proc do_callback {func args} { - variable shellspy_status_log - set exedir [file dirname [info nameofexecutable]] - set dispatchtcl [file join $exedir callbacks dispatch.tcl] - if {[file exists $dispatchtcl]} { - source $dispatchtcl - if {[llength [info commands shellspy::callbacks::$func]]} { - shellfilter::log::write $shellspy_status_log "found shellspy::callbacks::$func - calling" - if {[catch { - set args [shellspy::callbacks::$func {*}$args] - } errmsg]} { - shellfilter::log::write $shellspy_status_log "ERROR in shellspy::callbacks::$func\n errormsg:$errmsg" - error $errmsg - } - } - } - return $args - } - proc do_callback_parameters {func args} { - variable shellspy_status_log - set exedir [file dirname [info nameofexecutable]] - set paramtcl [file join $exedir callbacks parameters.tcl] - set params $args - if {[file exists $paramtcl]} { - source $paramtcl - if {[llength [info commands shellspy::parameters::$func]]} { - shellfilter::log::write $shellspy_status_log "found shellspy::parameters::$func - calling" - if {[catch { - set params [shellspy::parameters::$func $params] - } errmsg]} { - shellfilter::log::write $shellspy_status_log "ERROR in shellspy::parameters::$func\n errormsg:$errmsg" - } - } - } - return $params - } - - #some tested configs - proc get_channel_config {config} { - #note tcl script being called from wrong place.. configs don't affect: todo - move it. - set params [dict create] - if {$config == 0} { - #bad for: everything. extra cr - dict set params -inbuffering line - dict set params -outbuffering line - dict set params -readprocesstranslation auto ;#default - dict set params -outtranslation auto - } - - if {$config == 1} { - #ok for: cmd, cmd/u/c,raw,pwsh, sh,raw, tcl script process - #not ok for: bash,wsl, tcl script - dict set params -inbuffering line - dict set params -outbuffering line - dict set params -readprocesstranslation auto ;#default - dict set params -outtranslation lf - } - if {$config == 2} { - #ok for: cmd, cmd/uc,pwsh,sh , tcl script process - #not ok for: tcl script, bash, wsl - dict set params -inbuffering none ;#default - dict set params -outbuffering none ;#default - dict set params -readprocesstranslation auto ;#default - dict set params -outtranslation lf ;#default - } - if {$config == 3} { - #ok for: cmd - dict set params -inbuffering line - dict set params -outbuffering line - dict set params -readprocesstranslation lf - dict set params -outtranslation lf - } - if {$config == 4} { - #ok for: cmd,cmd/uc,raw,sh - #not ok for pwsh,bash,wsl, tcl script, tcl script process - dict set params -inbuffering none - dict set params -outbuffering none - dict set params -readprocesstranslation lf - dict set params -outtranslation lf - } - - if {$config == 5} { - #ok for: pwsh,cmd,cmd/u/c,raw,sh, tcl script process - #not ok for bash,wsl - #ok for vim cmd/u/c but only with to_unix filter on stdout (works in gvim and console) - dict set params -inbuffering none - dict set params -outbuffering none - dict set params -readprocesstranslation crlf - dict set params -outtranslation lf - } - if {$config == 6} { - #ok for: cmd,cmd/u/c,pwsh,raw,sh,bash - #not ok for: vim with cmd /u/c (?) - dict set params -inbuffering line - dict set params -outbuffering line - dict set params -readprocesstranslation crlf - dict set params -outtranslation lf - } - if {$config == 7} { - #ok for: sh,bash - #not ok for: wsl (display ok but extra cr), cmd,cmd/u/c,pwsh, tcl script, tcl script process, raw - dict set params -inbuffering none - dict set params -outbuffering none - dict set params -readprocesstranslation crlf - dict set params -outtranslation crlf - } - if {$config == 8} { - #not ok for anything..all have extra cr - dict set params -inbuffering none - dict set params -outbuffering none - dict set params -readprocesstranslation lf - dict set params -outtranslation crlf - } - return $params - } - - - - - proc do_in_powershell {args} { - variable shellspy_status_log - shellfilter::log::write $shellspy_status_log "do_in_powershell got '$args'" - set args [do_callback powershell {*}$args] - set params [do_callback_parameters powershell] - dict set params -teehandle shellspy - - - #readprocesstranslation lf - doesn't work for buffering line or none - #readprocesstranslation crlf works for buffering line and none with outchantranslation lf - - set params [dict merge $params [get_channel_config $::testconfig]] ;#working: 5 unbuffered, 6 linebuffered - - - - #set exitinfo [shellfilter::run [list pwsh -nologo -noprofile -c {*}$args] -debug 1] - - - set id_err [shellfilter::stack::add stderr ansiwrap -action sink -settings {-colour {red bold}}] - set exitinfo [shellfilter::run [list pwsh -nologo -noprofile -c {*}$args] {*}$params] - shellfilter::stack::remove stderr $id_err - - #Passing args in as a single element will tend to make powershell treat the args as a 'script block' - # (powershell sees items in curly brackets {} as a scriptblock - when called from non-powershell, this tends to just echo back the contents) - #set exitinfo [shellfilter::run [list pwsh -nologo -noprofile -c $args] {*}$params] - if {[lindex $exitinfo 0] eq "exitcode"} { - shellfilter::log::write $shellspy_status_log "do_in_powershell returning $exitinfo" - #exit [lindex $exitinfo 1] - } - return $exitinfo - } - proc do_in_powershell_terminal {args} { - variable shellspy_status_log - shellfilter::log::write $shellspy_status_log "do_in_powershell_terminal got '$args'" - set args [do_callback powershell {*}$args] - set params [do_callback_parameters powershell] - dict set params -teehandle shellspy - set params [dict merge $params [get_channel_config $::testconfig]] ;#working: 5 unbuffered, 6 linebuffered - - #set cmdlist [list pwsh -nologo -noprofile -c {*}$args] - set cmdlist [list pwsh -nologo -c {*}$args] - #the big problem with using the 'script' command is that we get stderr/stdout mashed together. - - #set cmdlist [shellfilter::get_scriptrun_from_cmdlist_dquote_if_not $cmdlist] - shellfilter::log::write $shellspy_status_log "do_in_powershell_terminal as script: '$cmdlist'" - - set id_err [shellfilter::stack::add stderr ansiwrap -action sink -settings {-colour {red bold}}] - set exitinfo [shellfilter::run $cmdlist {*}$params] - shellfilter::stack::remove stderr $id_err - - if {[lindex $exitinfo 0] eq "exitcode"} { - shellfilter::log::write $shellspy_status_log "do_in_powershell_terminal returning $exitinfo" - } - return $exitinfo - } - - - proc do_in_cmdshell {args} { - variable shellspy_status_log - shellfilter::log::write $shellspy_status_log "do_in_cmdshell got '$args'" - set args [do_callback cmdshell {*}$args] - set params [do_callback_parameters cmdshell] - - - dict set params -teehandle shellspy - dict set params -copytempfile 1 - - set params [dict merge $params [get_channel_config $::testconfig]] - - #set id_out [shellfilter::stack::add stdout tounix -action sink-locked -settings {}] - set id_err [shellfilter::stack::add stderr ansiwrap -action sink-locked -settings {-colour {red bold}}] - - #set exitinfo [shellfilter::run "cmd /c $args" -debug 1] - set exitinfo [shellfilter::run [list cmd /c {*}$args] {*}$params] - #set exitinfo [shellfilter::run [list cmd /c {*}$args] -debug 1] - - shellfilter::stack::remove stderr $id_err - - #shellfilter::stack::remove stdout $id_out - - - - if {[lindex $exitinfo 0] eq "exitcode"} { - #exit [lindex $exitinfo 1] - shellfilter::log::write $shellspy_status_log "do_in_cmdshell returning $exitinfo" - #puts stderr "do_in_cmdshell returning $exitinfo" - } - return $exitinfo - } - proc do_in_cmdshellb {args} { - - variable shellspy_status_log - shellfilter::log::write $shellspy_status_log "do_in_cmdshellb got '$args'" - - set args [do_callback cmdshellb {*}$args] - - - shellfilter::log::write $shellspy_status_log "do_in_cmdshellb post_callback args '$args'" - - set params [do_callback_parameters cmdshellb] - dict set params -teehandle shellspy - dict set params -copytempfile 1 - dict set params -debug 0 - - #----------------------------- - #channel config 6 and towindows sink-aside-locked {-junction 1} works with vim-flog - #----------------------------- - set params [dict merge $params [get_channel_config 6]] - #set id_out [shellfilter::stack::add stdout tounix -action sink-aside-locked -junction 1 -settings {}] - - - set exitinfo [shellfilter::run [list cmd /u/c {*}$args] {*}$params] - - #shellfilter::stack::remove stdout $id_out - - if {[lindex $exitinfo 0] eq "exitcode"} { - shellfilter::log::write $shellspy_status_log "do_in_cmdshellb returning with exitcode $exitinfo" - } else { - shellfilter::log::write $shellspy_status_log "do_in_cmdshellb returning WITHOUT exitcode $exitinfo" - } - return $exitinfo - } - proc do_in_cmdshelluc {args} { - variable shellspy_status_log - shellfilter::log::write $shellspy_status_log "do_in_cmdshelluc got '$args'" - set args [do_callback cmdshelluc {*}$args] - set params [do_callback_parameters cmdshell] - #set exitinfo [shellfilter::run "cmd /c $args" -debug 1] - dict set params -teehandle shellspy - dict set params -copytempfile 1 - dict set params -debug 0 - - #set params [dict merge $params [get_channel_config $::testconfig]] - - set params [dict merge $params [get_channel_config 1]] - #set id_out [shellfilter::stack::add stdout tounix -action sink-locked -settings {}] - - set id_out [shellfilter::stack::add stdout tounix -action sink-locked -settings {}] - - set exitinfo [shellfilter::run [list cmd /u/c {*}$args] {*}$params] - shellfilter::stack::remove stdout $id_out - #chan configure stdout -translation crlf - - if {[lindex $exitinfo 0] eq "exitcode"} { - #exit [lindex $exitinfo 1] - shellfilter::log::write $shellspy_status_log "do_in_cmdshelluc returning $exitinfo" - #puts stderr "do_in_cmdshell returning $exitinfo" - } - return $exitinfo - } - proc do_raw {args} { - variable shellspy_status_log - shellfilter::log::write $shellspy_status_log "do_raw got '$args'" - set args [do_callback raw {*}$args] - set params [do_callback_parameters raw] - #set params {} - dict set params -debug 0 - #dict set params -outprefix "_test_" - dict set params -teehandle shellspy - - - set params [dict merge $params [get_channel_config $::testconfig]] - - - if {[llength $params]} { - set exitinfo [shellfilter::run $args {*}$params] - } else { - set exitinfo [shellfilter::run $args -debug 1 -outprefix "j"] - } - if {[lindex $exitinfo 0] eq "exitcode"} { - shellfilter::log::write $shellspy_status_log "do_raw returning $exitinfo" - } - return $exitinfo - } - - proc do_script_process {scriptbin scriptname args} { - variable shellspy_status_log - shellfilter::log::write $shellspy_status_log "do_script_process got scriptname:'$scriptname' args:'$args'" - set args [do_callback script_process {*}$args] - set params [do_callback_parameters script_process] - dict set params -teehandle shellspy - - set params [dict merge $params [get_channel_config $::testconfig]] - - set exedir [file dirname [info nameofexecutable]] - set libroot [file join $exedir scriptlib] - if {[string match lib::* $scriptname]} { - set scriptname [string map [list "lib::" "" "::" "/"] $scriptname] - set scriptpath $libroot/$scriptname - } else { - set scriptpath $scriptname - } - if {![file exists $scriptpath]} { - set scriptpath [file rootname $scriptpath][string tolower [file extension $scriptpath]] - if {![file exists $scriptpath]} { - puts stderr "Failed to find script: '$scriptpath'" - error "bad scriptpath '$scriptpath'" - } - } - - - - set id_err [shellfilter::stack::add stderr ansiwrap -action sink-locked -settings {-colour {red bold}}] - - - - #todo - use glob to check capitalisation of file tail (.TCL vs .tcl .Tcl etc) - set exitinfo [shellfilter::run [concat [auto_execok $scriptbin] $scriptpath $args] {*}$params] - - shellfilter::stack::remove stderr $id_err - - if {[lindex $exitinfo 0] eq "exitcode"} { - shellfilter::log::write $shellspy_status_log "do_script_process returning $exitinfo" - } - if {[dict exists $exitinfo errorCode]} { - exit [dict get $exitinfo $errorCode] - } - return $exitinfo - } - proc do_script {scriptname replwhen args} { - #ideally we don't want to launch an external process to run the script - variable shellspy_status_log - shellfilter::log::write $shellspy_status_log "do_script got scriptname:'$scriptname' replwhen:$replwhen args:'$args'" - - set exedir [file dirname [info nameofexecutable]] - if {[file tail $exedir] eq "bin"} { - set basedir [file dirname $exedir] - } else { - set basedir $exedir - } - set libroot [file join $basedir scriptlib] - if {[string match lib::* $scriptname]} { - set scriptname [string map [list "lib::" "" "::" "/"] $scriptname] - set scriptpath $libroot/$scriptname - } else { - set scriptpath $scriptname - } - if {![file exists $scriptpath]} { - set scriptpath [file rootname $scriptpath][string tolower [file extension $scriptpath]] - if {![file exists $scriptpath]} { - puts stderr "Failed to find script: '$scriptpath'" - error "bad scriptpath '$scriptpath'" - } - } - set modulesdir $basedir/modules - - set script [string map [list %a% $args %s% $scriptpath %m% $modulesdir] { -::tcl::tm::add %m% -set scriptname %s% -set ::argv [list %a%] -set ::argc [llength $::argv] -source [file normalize $scriptname] - - }] - - set repl_lines "" - #append repl_lines {puts stderr "starting repl [chan names]"} \n - #append repl_lines {puts stderr "stdin [chan configure stdin]"} \n - append repl_lines {package require punk::repl} \n - append repl_lines {repl::start stdin} \n - - #append repl_lines {puts stdout "shutdown message"} \n - - if {$replwhen eq "repl_first"} { - #we need to cooperate with the repl to get the script to run on exit - namespace eval ::repl {} - set ::repl::post_script $script - set script "$repl_lines" - } elseif {$replwhen eq "repl_last"} { - append script $repl_lines - } else { - #just the script - } - - - set args [do_callback script {*}$args] - set params [do_callback_parameters script] - dict set params -tclscript 1 ;#don't give callback a chance to omit/break this - dict set params -teehandle shellspy - - set params [dict merge $params [get_channel_config $::testconfig]] - - set id_err [shellfilter::stack::add stderr ansiwrap -action sink-locked -settings {-colour {red bold}}] - - set exitinfo [shellfilter::run $script {*}$params] - - shellfilter::stack::remove stderr $id_err - - #if {[lindex $exitinfo 0] eq "exitcode"} { - # shellfilter::log::write $shellspy_status_log "do_script returning $exitinfo" - #} - - shellfilter::log::write $shellspy_status_log "do_script returning $exitinfo" - return $exitinfo - } - - proc shellescape {arglist} { - set out [list] - foreach a $arglist { - set a [string map [list \\ \\\\ ] $a] - lappend out $a - } - return $out - } - proc do_shell {shell args} { - variable shellspy_status_log - shellfilter::log::write $shellspy_status_log "do_shell $shell got '$args' [llength $args]" - set args [do_callback $shell {*}$args] - shellfilter::log::write $shellspy_status_log "do_shell $shell xgot '$args'" - set params [do_callback_parameters $shell] - dict set params -teehandle shellspy - - - set params [dict merge $params [get_channel_config $::testconfig]] - - set id_out [shellfilter::stack::add stdout towindows -action sink-aside-locked -junction 1 -settings {}] - - #shells that take -c and need all args passed together as a string - - set exitinfo [shellfilter::run [concat $shell -c [shellescape $args]] {*}$params] - - shellfilter::stack::remove stdout $id_out - - - if {[lindex $exitinfo 0] eq "exitcode"} { - shellfilter::log::write $shellspy_status_log "do_shell $shell returning $exitinfo" - } - return $exitinfo - } - proc do_wsl {dist args} { - variable shellspy_status_log - shellfilter::log::write $shellspy_status_log "do_wsl $dist got '$args' [llength $args]" - set args [do_callback wsl {*}$args] ;#use dist? - shellfilter::log::write $shellspy_status_log "do_wsl $dist xgot '$args'" - set params [do_callback_parameters wsl] - - dict set params -debug 0 - - - set params [dict merge $params [get_channel_config $::testconfig]] - - - set id_out [shellfilter::stack::add stdout towindows -action sink-aside-locked -junction 1 -settings {}] - - - dict set params -teehandle shellspy ;#shellspyout shellspyerr must exist - set exitinfo [shellfilter::run [concat wsl -d $dist -e [shellescape $args]] {*}$params] - - - shellfilter::stack::remove stdout $id_out - - - if {[lindex $exitinfo 0] eq "exitcode"} { - shellfilter::log::write $shellspy_status_log "do_wsl $dist returning $exitinfo" - } - return $exitinfo - } - - #todo - load these from a callback - set commands [list] - - #shout extension to force use of tclsh as a separate process - #todo - detect various extensions - and use all script-preceding unmatched args as interpreter+options - #e.g perl,php,python etc. - #For tcl will make it easy to test different interpreter output from tclkitsh, tclsh8.6, tclsh8.7 etc - #for now we have a hardcoded default interpreter for tcl as 'tclsh' - todo: get default interps from config - #(or just attempt launch in case there is shebang line in script) - #we may get ambiguity with existing shell match-specs such as -c /c -r. todo - only match those in first slot? - lappend commands [list tclscriptprocess [list match [list .*\.TCL$ .*\.TM$ .*\.TK$] dispatch [list shellspy::do_script_process tclsh %matched%] dispatchtype raw dispatchglobal 1 singleopts {any}]] - for {set i 0} {$i < 25} {incr i} { - lappend commands [list tclscriptprocess [list sub word$i singleopts {any}]] - } - - #camelcase convention .Tcl script before repl - lappend commands [list tclscriptbeforerepl [list match [list .*\.Tcl$ .*\.Tm$ .*\.Tk$ ] dispatch [list shellspy::do_script %matched% "repl_last"] dispatchtype raw dispatchglobal 1 singleopts {any}]] - for {set i 0} {$i < 25} {incr i} { - lappend commands [list tclscriptbeforerepl [list sub word$i singleopts {any}]] - } - - - #Backwards Camelcase convention .tcL - means repl first, script last - lappend commands [list tclscriptafterrepl [list match [list .*\.tcL$ .*\.tM$ .*\.tK$ ] dispatch [list shellspy::do_script %matched% "repl_first"] dispatchtype raw dispatchglobal 1 singleopts {any}]] - for {set i 0} {$i < 25} {incr i} { - lappend commands [list tclscriptafterrepl [list sub word$i singleopts {any}]] - } - - - #we've already handled .Tcl .tcL, .tCl, .TCL - handle any other capitalisations as a script in this process - lappend commands [list tclscript [list match [list .*\.tcl$ .*\.tCL$ .*\.TCl$ .*\.tm$ .*\.tk$ ] dispatch [list shellspy::do_script %matched% "no_repl"] dispatchtype raw dispatchglobal 1 singleopts {any}]] - for {set i 0} {$i < 25} {incr i} { - lappend commands [list tclscript [list sub word$i singleopts {any}]] - } - - - lappend commands [list luascriptprocess [list match [list .*\.lua|Lua|LUA$] dispatch [list shellspy::do_script_process lua %matched% "no_repl"] dispatchtype raw dispatchglobal 1 singleopts {any}]] - for {set i 0} {$i < 25} {incr i} { - lappend commands [list luascriptprocess [list sub word$i singleopts {any}]] - } - - lappend commands [list phpscriptprocess [list match [list .*\.php|Php|PHP$] dispatch [list shellspy::do_script_process php %matched% "no_repl"] dispatchtype raw dispatchglobal 1 singleopts {any}]] - for {set i 0} {$i < 25} {incr i} { - lappend commands [list phpscriptprocess [list sub word$i singleopts {any}]] - } - - lappend commands [list bashraw [list match ^bash$ dispatch [list shellspy::do_shell bash] dispatchtype raw dispatchglobal 1 singleopts {any}]] - for {set i 0} {$i < 25} {incr i} { - lappend commands [list bashraw [list sub word$i singleopts {any}]] - } - lappend commands {shraw {match ^sh$ dispatch {shellspy::do_shell sh} dispatchtype raw dispatchglobal 1 singleopts {any}}} - for {set i 0} {$i < 25} {incr i} { - lappend commands [list shraw [list sub word$i singleopts {any}]] - } - - lappend commands [list runbash [list match ^b$ dispatch [list shellspy::do_shell bash] dispatchtype shell dispatchglobal 1 singleopts {any}]] - for {set i 0} {$i < 25} {incr i} { - lappend commands [list runbash [list sub word$i singleopts {any}]] - } - lappend commands {runsh {match ^s$ dispatch {shellspy::do_shell sh} dispatchtype shell dispatchglobal 1 singleopts {any}}} - for {set i 0} {$i < 25} {incr i} { - lappend commands [list runsh [list sub word$i singleopts {any}]] - } - - lappend commands {runraw {match ^-r$ dispatch shellspy::do_raw dispatchtype raw dispatchglobal 1 singleopts {any}}} - for {set i 0} {$i < 25} {incr i} { - lappend commands [list runraw [list sub word$i singleopts {any}]] - } - lappend commands {runpwsh {match ^-c$ dispatch shellspy::do_in_powershell dispatchtype raw dispatchglobal 1 singleopts {any}}} - for {set i 0} {$i < 25} {incr i} { - lappend commands [list runpwsh [list sub word$i singleopts {any}]] - } - lappend commands {runpwsht {match ^pwsh$ dispatch shellspy::do_in_powershell_terminal dispatchtype raw dispatchglobal 1 singleopts {any}}} - for {set i 0} {$i < 25} {incr i} { - lappend commands [list runpwsht [list sub word$i singleopts {any}]] - } - - - lappend commands {runcmd {match ^/c$ dispatch shellspy::do_in_cmdshell dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}}} - for {set i 0} {$i < 25} {incr i} { - lappend commands [list runcmd [list sub word$i singleopts {any}]] - } - lappend commands {runcmduc {match ^/u/c$ dispatch shellspy::do_in_cmdshelluc dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}}} - for {set i 0} {$i < 25} {incr i} { - lappend commands [list runcmduc [list sub word$i singleopts {any}]] - } - #cmd with bracked args () e.g with vim shellxquote set to "(" - lappend commands [list runcmdb [list match ^cmdb$ dispatch [list shellspy::do_in_cmdshellb %matched%] dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any} pairopts {any}]] - for {set i 0} {$i < 25} {incr i} { - lappend commands [list runcmdb [list sub word$i singleopts {any} longopts {any} pairopts {any}]] - } - - lappend commands [list wslraw [list match ^wsl$ dispatch [list shellspy::do_wsl AlmaLinux9] dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}]] - for {set i 0} {$i < 25} {incr i} { - lappend commands [list wslraw [list sub word$i singleopts {any}]] - } - - - ############################################################################################ - - #echo raw args to diverted stderr before running the argument analysis - puts -nonewline stderr "exe:[info nameofexecutable] script:[info script] shellspy-rawargs: $::argv\n" - set i 1 - foreach a $::argv { - puts -nonewline stderr "arg$i: '$a'\n" - incr i - } - - - puts stderr "ansi-test:$::shellfilter::ansi::test" - flush stderr - - set argdefinitions [list \ - -caller shellspy_dispatcher \ - -debugargs 0 \ - -debugargsonerror 2 \ - -return all \ - -soloflags {} \ - -defaults [list] \ - -required {none} \ - -extras {all} \ - -commandprocessors $commands \ - -values $::argv ] - - - - if {[catch { - set arglist [check_flags {*}$argdefinitions] - } errMsg]} { - puts -nonewline stderr "|shellspy-stderr> ERROR during command dispatch\n" - puts -nonewline stderr "|shellspy-stderr> $errMsg\n" - puts -nonewline stderr "|shellspy-stderr> [set ::errorInfo]\n" - - shellfilter::log::write $shellspy_status_log "check_flags error: $errMsg" - } else { - shellfilter::log::write $shellspy_status_log "check_flags result: $arglist" - } - - shellfilter::log::write $shellspy_status_log "check_flags dispatch -done-" - - #puts stdout "sp2. $::argv" - - if {[catch { - set tidyinfo [shellfilter::logtidyup] - } errMsg]} { - - shellfilter::log::open shellspy-error {-tag shellspy-error -syslog 127.0.0.1:514} - shellfilter::log::write shellspy-error "logtidyup error $errMsg\n [set ::errorInfo]" - after 200 - } - #don't open more logs.. - #puts stdout ">$tidyinfo" - - - #shellfilter::log::write $shellspy_status_log "logtidyup -done- $tidyinfo" - - set errorlist [dict get $tidyinfo errors] - if {[llength $errorlist]} { - foreach err $errorlist { - puts -nonewline stderr "|shellspy-final> worker-error-set $err\n" - } - } - puts stdout "shellspy -done-" - #shellfilter::log::write $shellspy_status_log "shellspy -done-" - flush stdout - - if {[catch { - shellfilter::logtidyup $shellspy_status_log - } errMsg]} { - shellfilter::log::open shellspy-final {-tag shellspy-final -syslog 127.0.0.1:514} - shellfilter::log::write shellspy-final "FINAL logtidyup error $errMsg\n [set ::errorInfo]" - after 200 - - } - #if we call exit - package require Tk scripts will exit prematurely - #exit 0 -} - -} +#! /usr/bin/env tclsh +# +#copyright 2023 Julian Marcel Noble +#license: BSD (revised 3-clause) +# +#see notes at beginning of shellspy namespace re stdout/stderr +# +#SHELLSPY - provides commandline flag information and stdout/stderr logging/mirroring without output/pipeline interference, +# or modified output if modifying filters explicitly configured. +# +#shellspy uses the shellfilter and flagfilter libraries to run a commandline with tee-diversions of stdout/stderr to configured syslog/file logs +#Because it is a tee, the command's stdout/stderr are still available as direct output from this script. +#Various filters/modifiers/redirections can be placed on the channel stacks for stdin/stderr using the shellfilter::stack api +# and other shellfilter:: helpers such as shellfilter::redir_output_to_log +# Redirecting stderr/stdout in this way prior to the actual command run will not interfere with the command/pipeline output due to the way +# shellfilter temporarily inserts it's own tee into the stack at the point of the highest existing redirection it encounters. +# +#A note on input/output convention regarding channels/pipes +# we write to an output, read from an input. +# e.g when creating a pipe with 'lassign [chan pipe] in out' we write to out and read from in. +# This is potentially confusing from an OO-like perspective thinking of a pipe object as a sort of object. +# Don't think of it from the perspective of the pipe - but from the program using it. +# This is not a convention invented here for shellspy - but it seems to match documentation and common use e.g for 'chan pending input|output chanid' +# This matches the way we write to stdout read from stdin. +# Possibly using read/write related naming for pipe ends is clearer. eg 'lassign [chan pipe] rd_pipe1 wr_pipe1' +# +package provide app-shellspy 1.0 + + +#a test for windows +#fconfigure stdin -encoding utf-16le +#fconfigure stdout -encoding utf-16le + +#tcl::tm::remove {*}[tcl::tm::list] +set original_tm_list [tcl::tm::list] +tcl::tm::remove {*}$original_tm_list + +if {[string match "*.vfs/*" [info script]]} { + #src/xxx.vfs/lib/app-punk/repl.tcl + #we assume if calling directly into .vfs that the user would prefer to use src/modules - so go up 4 levels + set m_dir [file dirname [file dirname [file dirname [file dirname [info script]]]]]/modules +} else { + #add dir outside of tclkit/exe so we can override with higher versions if necessary without rebuilding + # .../bin/punkXX.exe look for ../modules (i.e modules folder at same level as bin folder) + set m_dir [file normalize [file join [file dirname [file dirname [info nameofexecutable]]] modules]] +} +tcl::tm::add $m_dir +#set m_dir [file normalize [file join [file dirname [info script]] ../../../modules]] +#tcl::tm::add $m_dir +set libfolder [file dirname [file dirname [info nameofexecutable]]]/lib +if {[file exists $libfolder]} { + lappend ::auto_path $libfolder +} +#experiment - todo make a flag for it if it's useful +#Middle cap for direct dispatch without flagcheck arg processing or redirections or REPL. +set arg1 [lindex $::argv 0] +if {[file extension $arg1] in [list .tCl]} { + set ::argv [lrange $::argv 1 end] + set ::argc [llength $::argv] + + set exedir [file dirname [info nameofexecutable]] + set libroot [file join $exedir scriptlib] + set scriptname $arg1 + if {[string match lib::* $scriptname]} { + set scriptname [string map [list "lib::" "" "::" "/"] $scriptname] + set scriptpath $libroot/$scriptname + } else { + set scriptpath [file normalize $scriptname] + } + + if {![file exists $scriptpath]} { + #try the lowercase version (extension lowercased only) so that file doesn't have to be renamed to use alternate dispatch + set scriptpath [file rootname $scriptpath][string tolower [file extension $scriptpath]] + } + source $scriptpath + + #package require app-punk + +} else { + + + +#set m_dir [file join $starkit::topdir modules] + +#lappend auto_path c:/tcl/lib/tcllib1.20 + +package require Thread + +#NOTE: tm package index will probably already have been created so we must use 'package forget' to restrict to current tcl::tm::list path +#Review - effect on load time of wasting a previously created index? better way? +#require core modules only from punk distribution (REVIEW - override option?) +package forget flagfilter +package require flagfilter +package forget shellfilter +package require shellfilter +package forget punk +package require punk + +#restore module paths +set tm_list_now [tcl::tm::list] +foreach p $original_tm_list { + if {$p ni $tm_list_now} { + tcl::tm::add $p + } +} + + +#package require packageTrace + +set ::testconfig 5 + +namespace eval shellspy { + variable shellspy_status_log "shellspy-[clock micros]" + #shellfilter::log::open $shellspy_status_log [list -tag $shellspy_status_log -syslog 172.16.6.42:51500 -file ""] + shellfilter::log::open $shellspy_status_log [list -tag $shellspy_status_log -syslog 127.0.0.1:514 -file ""] + shellfilter::log::write $shellspy_status_log "shellspy launch with args '$::argv'" + shellfilter::log::write $shellspy_status_log "stdout/stderr encoding: [chan configure stdout -encoding]/[chan configure stderr -encoding]" + + #------------------------------------------------------------------------- + ##don't write to stdout/stderr before you've redirected them to a log using shellfilter functions + ## puts to stdout/stderr will comingle with command's output if performed before the channel stacks are configured. + + chan configure stdin -buffering line + chan configure stdout -buffering none + chan configure stderr -buffering none + + #set id_ansistrip [shellfilter::stack::add stderr ansistrip -settings {}] + #set id_ansistrip [shellfilter::stack::add stdout ansistrip -settings {}] + + lassign [shellfilter::redir_output_to_log "SPY"] id_stdout_redir id_stderr_redir + + + ### + #we can now safely write to stderr/stdout and it will not interfere with stderr/stdout from the dispatched command. + #This is because when shellfilter::run is called it temporarily adds it's own filter in place of the redirection we just added. + # shellfilter::run installs tee_to_pipe on stdout & stderr with -action sink-aside. + # sink-aside means to sink down the filter stack to the topmost filter that is diversionary, and replace it. + # when the command finishes running, the redirecting filter that it displaced is reinstalled in the stack. + ### + + ### + #Note that futher filters installed here will sit 'above' any of the redirecting filters + # so apply to both the shellfilter::run commandline, + # as well as writes to stderr/stdout from here or other libraries operating in this process. + # To bypass the the filter-stack and still emit to syslog etc - + # you can use shellfilter::log::open and shellfilter::log::write e.g + # shellfilter::log::open "mystatuslog" [list -tag "mystatuslog" -syslog 172.16.6.42:51500 -file ""] + # shellfilter::log::write "mystatuslog" "shellspy launch" + # + #### + #set id_ansistrip [shellfilter::stack::add stderr ansistrip -action float -settings {}] + #set id_ansistrip [shellfilter::stack::add stdout ansistrip -action float -settings {}] + + + ##stdin stack operates in reverse compared to stdout/stderr in that first added to stack is first to see the data + ##for stdin: first added to stack is 'upstream' as it will always encounter the data before later ones added to the stack. + ##for stdout: first added to stack is 'downstream' as it will alays encounter the data after others on the stack + #shellfilter::stack::add stdin ansistrip -action {} -settings {} + #shellfilter::stack::add stdin tee_to_log -settings {-tag "input" -raw 1 -syslog 172.16.6.42:51500 -file ""} + + #------------------------------------------------------------------------- + ##note - to use shellfilter::stack::new - the fifo2 end at the local side requires the event loop to be running + ## for interactive testing a relatively simple repl.tcl can be used. + + #todo - default to no logging.. add special flag in checkflags to indicate end of options for matched command only e.g --- ? + # then prioritize these results from checkflags to configure pre-dispatch behaviour by running a predispatch script(?) + # + # we can log flag-processing info coming from checkflags.. but we don't want to do two opt scans and emit it twice. + # configuration of the logging for flag/opt parsing should come from a config file and default to none. + set stdout_log [file normalize ~]/shellspy-stdout.txt + set stderr_log [file normalize ~]/shellspy-stderr.txt + + set outdeviceinfo [shellfilter::stack::new shellspyout -settings [list -tag "shellspyout" -buffering none -raw 1 -syslog 127.0.0.1:514 -file $stdout_log]] + set commandlog [dict get $outdeviceinfo localchan] + #puts $commandlog "HELLO $commandlog" + #flush $commandlog + set errdeviceinfo [shellfilter::stack::new shellspyerr -settings [list -tag "shellspyerr" -buffering none -raw 1 -syslog 127.0.0.1:514 -file $stderr_log]] + + #note that this filter is inline with the data teed off to the shellspyout log. + #To filter the main stdout an addition to the stdout stack can be made. specify -action float to have it affect both stdout and the tee'd off data. + set id_ansistrip [shellfilter::stack::add shellspyout ansistrip -settings {}] + + + #set id_out [shellfilter::stack::add stdout rebuffer -settings {}] + + #an example filter to capture some output to a var for later use - this one is for ansible-playbook + #set ::recap "" + #set id_tee_grep [shellfilter::stack::add shellspyout tee_grep_to_var -settings {-grep ".*PLAY RECAP.*|.*fatal:.*|.*changed:.*" -varname ::recap -pre 1 -post 3}] + + namespace import ::flagfilter::check_flags + + namespace eval shellspy::callbacks {} + namespace eval shellspy::parameters {} + + + proc do_callback {func args} { + variable shellspy_status_log + set exedir [file dirname [info nameofexecutable]] + set dispatchtcl [file join $exedir callbacks dispatch.tcl] + if {[file exists $dispatchtcl]} { + source $dispatchtcl + if {[llength [info commands shellspy::callbacks::$func]]} { + shellfilter::log::write $shellspy_status_log "found shellspy::callbacks::$func - calling" + if {[catch { + set args [shellspy::callbacks::$func {*}$args] + } errmsg]} { + shellfilter::log::write $shellspy_status_log "ERROR in shellspy::callbacks::$func\n errormsg:$errmsg" + error $errmsg + } + } + } + return $args + } + proc do_callback_parameters {func args} { + variable shellspy_status_log + set exedir [file dirname [info nameofexecutable]] + set paramtcl [file join $exedir callbacks parameters.tcl] + set params $args + if {[file exists $paramtcl]} { + source $paramtcl + if {[llength [info commands shellspy::parameters::$func]]} { + shellfilter::log::write $shellspy_status_log "found shellspy::parameters::$func - calling" + if {[catch { + set params [shellspy::parameters::$func $params] + } errmsg]} { + shellfilter::log::write $shellspy_status_log "ERROR in shellspy::parameters::$func\n errormsg:$errmsg" + } + } + } + return $params + } + + #some tested configs + proc get_channel_config {config} { + #note tcl script being called from wrong place.. configs don't affect: todo - move it. + set params [dict create] + if {$config == 0} { + #bad for: everything. extra cr + dict set params -inbuffering line + dict set params -outbuffering line + dict set params -readprocesstranslation auto ;#default + dict set params -outtranslation auto + } + + if {$config == 1} { + #ok for: cmd, cmd/u/c,raw,pwsh, sh,raw, tcl script process + #not ok for: bash,wsl, tcl script + dict set params -inbuffering line + dict set params -outbuffering line + dict set params -readprocesstranslation auto ;#default + dict set params -outtranslation lf + } + if {$config == 2} { + #ok for: cmd, cmd/uc,pwsh,sh , tcl script process + #not ok for: tcl script, bash, wsl + dict set params -inbuffering none ;#default + dict set params -outbuffering none ;#default + dict set params -readprocesstranslation auto ;#default + dict set params -outtranslation lf ;#default + } + if {$config == 3} { + #ok for: cmd + dict set params -inbuffering line + dict set params -outbuffering line + dict set params -readprocesstranslation lf + dict set params -outtranslation lf + } + if {$config == 4} { + #ok for: cmd,cmd/uc,raw,sh + #not ok for pwsh,bash,wsl, tcl script, tcl script process + dict set params -inbuffering none + dict set params -outbuffering none + dict set params -readprocesstranslation lf + dict set params -outtranslation lf + } + + if {$config == 5} { + #ok for: pwsh,cmd,cmd/u/c,raw,sh, tcl script process + #not ok for bash,wsl + #ok for vim cmd/u/c but only with to_unix filter on stdout (works in gvim and console) + dict set params -inbuffering none + dict set params -outbuffering none + dict set params -readprocesstranslation crlf + dict set params -outtranslation lf + } + if {$config == 6} { + #ok for: cmd,cmd/u/c,pwsh,raw,sh,bash + #not ok for: vim with cmd /u/c (?) + dict set params -inbuffering line + dict set params -outbuffering line + dict set params -readprocesstranslation crlf + dict set params -outtranslation lf + } + if {$config == 7} { + #ok for: sh,bash + #not ok for: wsl (display ok but extra cr), cmd,cmd/u/c,pwsh, tcl script, tcl script process, raw + dict set params -inbuffering none + dict set params -outbuffering none + dict set params -readprocesstranslation crlf + dict set params -outtranslation crlf + } + if {$config == 8} { + #not ok for anything..all have extra cr + dict set params -inbuffering none + dict set params -outbuffering none + dict set params -readprocesstranslation lf + dict set params -outtranslation crlf + } + return $params + } + + + + + proc do_in_powershell {args} { + variable shellspy_status_log + shellfilter::log::write $shellspy_status_log "do_in_powershell got '$args'" + set args [do_callback powershell {*}$args] + set params [do_callback_parameters powershell] + dict set params -teehandle shellspy + + + #readprocesstranslation lf - doesn't work for buffering line or none + #readprocesstranslation crlf works for buffering line and none with outchantranslation lf + + set params [dict merge $params [get_channel_config $::testconfig]] ;#working: 5 unbuffered, 6 linebuffered + + + + #set exitinfo [shellfilter::run [list pwsh -nologo -noprofile -c {*}$args] -debug 1] + + + set id_err [shellfilter::stack::add stderr ansiwrap -action sink -settings {-colour {red bold}}] + set exitinfo [shellfilter::run [list pwsh -nologo -noprofile -c {*}$args] {*}$params] + shellfilter::stack::remove stderr $id_err + + #Passing args in as a single element will tend to make powershell treat the args as a 'script block' + # (powershell sees items in curly brackets {} as a scriptblock - when called from non-powershell, this tends to just echo back the contents) + #set exitinfo [shellfilter::run [list pwsh -nologo -noprofile -c $args] {*}$params] + if {[lindex $exitinfo 0] eq "exitcode"} { + shellfilter::log::write $shellspy_status_log "do_in_powershell returning $exitinfo" + #exit [lindex $exitinfo 1] + } + return $exitinfo + } + proc do_in_powershell_terminal {args} { + variable shellspy_status_log + shellfilter::log::write $shellspy_status_log "do_in_powershell_terminal got '$args'" + set args [do_callback powershell {*}$args] + set params [do_callback_parameters powershell] + dict set params -teehandle shellspy + set params [dict merge $params [get_channel_config $::testconfig]] ;#working: 5 unbuffered, 6 linebuffered + + #set cmdlist [list pwsh -nologo -noprofile -c {*}$args] + set cmdlist [list pwsh -nologo -c {*}$args] + #the big problem with using the 'script' command is that we get stderr/stdout mashed together. + + #set cmdlist [shellfilter::get_scriptrun_from_cmdlist_dquote_if_not $cmdlist] + shellfilter::log::write $shellspy_status_log "do_in_powershell_terminal as script: '$cmdlist'" + + set id_err [shellfilter::stack::add stderr ansiwrap -action sink -settings {-colour {red bold}}] + set exitinfo [shellfilter::run $cmdlist {*}$params] + shellfilter::stack::remove stderr $id_err + + if {[lindex $exitinfo 0] eq "exitcode"} { + shellfilter::log::write $shellspy_status_log "do_in_powershell_terminal returning $exitinfo" + } + return $exitinfo + } + + + proc do_in_cmdshell {args} { + variable shellspy_status_log + shellfilter::log::write $shellspy_status_log "do_in_cmdshell got '$args'" + set args [do_callback cmdshell {*}$args] + set params [do_callback_parameters cmdshell] + + + dict set params -teehandle shellspy + dict set params -copytempfile 1 + + set params [dict merge $params [get_channel_config $::testconfig]] + + #set id_out [shellfilter::stack::add stdout tounix -action sink-locked -settings {}] + set id_err [shellfilter::stack::add stderr ansiwrap -action sink-locked -settings {-colour {red bold}}] + + #set exitinfo [shellfilter::run "cmd /c $args" -debug 1] + set exitinfo [shellfilter::run [list cmd /c {*}$args] {*}$params] + #set exitinfo [shellfilter::run [list cmd /c {*}$args] -debug 1] + + shellfilter::stack::remove stderr $id_err + + #shellfilter::stack::remove stdout $id_out + + + + if {[lindex $exitinfo 0] eq "exitcode"} { + #exit [lindex $exitinfo 1] + shellfilter::log::write $shellspy_status_log "do_in_cmdshell returning $exitinfo" + #puts stderr "do_in_cmdshell returning $exitinfo" + } + return $exitinfo + } + proc do_in_cmdshellb {args} { + + variable shellspy_status_log + shellfilter::log::write $shellspy_status_log "do_in_cmdshellb got '$args'" + + set args [do_callback cmdshellb {*}$args] + + + shellfilter::log::write $shellspy_status_log "do_in_cmdshellb post_callback args '$args'" + + set params [do_callback_parameters cmdshellb] + dict set params -teehandle shellspy + dict set params -copytempfile 1 + dict set params -debug 0 + + #----------------------------- + #channel config 6 and towindows sink-aside-locked {-junction 1} works with vim-flog + #----------------------------- + set params [dict merge $params [get_channel_config 6]] + #set id_out [shellfilter::stack::add stdout tounix -action sink-aside-locked -junction 1 -settings {}] + + + set exitinfo [shellfilter::run [list cmd /u/c {*}$args] {*}$params] + + #shellfilter::stack::remove stdout $id_out + + if {[lindex $exitinfo 0] eq "exitcode"} { + shellfilter::log::write $shellspy_status_log "do_in_cmdshellb returning with exitcode $exitinfo" + } else { + shellfilter::log::write $shellspy_status_log "do_in_cmdshellb returning WITHOUT exitcode $exitinfo" + } + return $exitinfo + } + proc do_in_cmdshelluc {args} { + variable shellspy_status_log + shellfilter::log::write $shellspy_status_log "do_in_cmdshelluc got '$args'" + set args [do_callback cmdshelluc {*}$args] + set params [do_callback_parameters cmdshell] + #set exitinfo [shellfilter::run "cmd /c $args" -debug 1] + dict set params -teehandle shellspy + dict set params -copytempfile 1 + dict set params -debug 0 + + #set params [dict merge $params [get_channel_config $::testconfig]] + + set params [dict merge $params [get_channel_config 1]] + #set id_out [shellfilter::stack::add stdout tounix -action sink-locked -settings {}] + + set id_out [shellfilter::stack::add stdout tounix -action sink-locked -settings {}] + + set exitinfo [shellfilter::run [list cmd /u/c {*}$args] {*}$params] + shellfilter::stack::remove stdout $id_out + #chan configure stdout -translation crlf + + if {[lindex $exitinfo 0] eq "exitcode"} { + #exit [lindex $exitinfo 1] + shellfilter::log::write $shellspy_status_log "do_in_cmdshelluc returning $exitinfo" + #puts stderr "do_in_cmdshell returning $exitinfo" + } + return $exitinfo + } + proc do_raw {args} { + variable shellspy_status_log + shellfilter::log::write $shellspy_status_log "do_raw got '$args'" + set args [do_callback raw {*}$args] + set params [do_callback_parameters raw] + #set params {} + dict set params -debug 0 + #dict set params -outprefix "_test_" + dict set params -teehandle shellspy + + + set params [dict merge $params [get_channel_config $::testconfig]] + + + if {[llength $params]} { + set exitinfo [shellfilter::run $args {*}$params] + } else { + set exitinfo [shellfilter::run $args -debug 1 -outprefix "j"] + } + if {[lindex $exitinfo 0] eq "exitcode"} { + shellfilter::log::write $shellspy_status_log "do_raw returning $exitinfo" + } + return $exitinfo + } + + proc do_script_process {scriptbin scriptname args} { + variable shellspy_status_log + shellfilter::log::write $shellspy_status_log "do_script_process got scriptname:'$scriptname' args:'$args'" + set args [do_callback script_process {*}$args] + set params [do_callback_parameters script_process] + dict set params -teehandle shellspy + + set params [dict merge $params [get_channel_config $::testconfig]] + + set exedir [file dirname [info nameofexecutable]] + set libroot [file join $exedir scriptlib] + if {[string match lib::* $scriptname]} { + set scriptname [string map [list "lib::" "" "::" "/"] $scriptname] + set scriptpath $libroot/$scriptname + } else { + set scriptpath $scriptname + } + if {![file exists $scriptpath]} { + set scriptpath [file rootname $scriptpath][string tolower [file extension $scriptpath]] + if {![file exists $scriptpath]} { + puts stderr "Failed to find script: '$scriptpath'" + error "bad scriptpath '$scriptpath'" + } + } + + + + set id_err [shellfilter::stack::add stderr ansiwrap -action sink-locked -settings {-colour {red bold}}] + + + + #todo - use glob to check capitalisation of file tail (.TCL vs .tcl .Tcl etc) + set exitinfo [shellfilter::run [concat [auto_execok $scriptbin] $scriptpath $args] {*}$params] + + shellfilter::stack::remove stderr $id_err + + if {[lindex $exitinfo 0] eq "exitcode"} { + shellfilter::log::write $shellspy_status_log "do_script_process returning $exitinfo" + } + if {[dict exists $exitinfo errorCode]} { + exit [dict get $exitinfo $errorCode] + } + return $exitinfo + } + proc do_script {scriptname replwhen args} { + #ideally we don't want to launch an external process to run the script + variable shellspy_status_log + shellfilter::log::write $shellspy_status_log "do_script got scriptname:'$scriptname' replwhen:$replwhen args:'$args'" + + set exedir [file dirname [info nameofexecutable]] + if {[file tail $exedir] eq "bin"} { + set basedir [file dirname $exedir] + } else { + set basedir $exedir + } + set libroot [file join $basedir scriptlib] + if {[string match lib::* $scriptname]} { + set scriptname [string map [list "lib::" "" "::" "/"] $scriptname] + set scriptpath $libroot/$scriptname + } else { + set scriptpath $scriptname + } + if {![file exists $scriptpath]} { + set scriptpath [file rootname $scriptpath][string tolower [file extension $scriptpath]] + if {![file exists $scriptpath]} { + puts stderr "Failed to find script: '$scriptpath'" + error "bad scriptpath '$scriptpath'" + } + } + set modulesdir $basedir/modules + + set script [string map [list %a% $args %s% $scriptpath %m% $modulesdir] { +::tcl::tm::add %m% +set scriptname %s% +set ::argv [list %a%] +set ::argc [llength $::argv] +source [file normalize $scriptname] + + }] + + set repl_lines "" + #append repl_lines {puts stderr "starting repl [chan names]"} \n + #append repl_lines {puts stderr "stdin [chan configure stdin]"} \n + append repl_lines {package require punk::repl} \n + append repl_lines {repl::start stdin} \n + + #append repl_lines {puts stdout "shutdown message"} \n + + if {$replwhen eq "repl_first"} { + #we need to cooperate with the repl to get the script to run on exit + namespace eval ::repl {} + set ::repl::post_script $script + set script "$repl_lines" + } elseif {$replwhen eq "repl_last"} { + append script $repl_lines + } else { + #just the script + } + + + set args [do_callback script {*}$args] + set params [do_callback_parameters script] + dict set params -tclscript 1 ;#don't give callback a chance to omit/break this + dict set params -teehandle shellspy + + set params [dict merge $params [get_channel_config $::testconfig]] + + set id_err [shellfilter::stack::add stderr ansiwrap -action sink-locked -settings {-colour {red bold}}] + + set exitinfo [shellfilter::run $script {*}$params] + + shellfilter::stack::remove stderr $id_err + + #if {[lindex $exitinfo 0] eq "exitcode"} { + # shellfilter::log::write $shellspy_status_log "do_script returning $exitinfo" + #} + + shellfilter::log::write $shellspy_status_log "do_script returning $exitinfo" + return $exitinfo + } + + proc shellescape {arglist} { + set out [list] + foreach a $arglist { + set a [string map [list \\ \\\\ ] $a] + lappend out $a + } + return $out + } + proc do_shell {shell args} { + variable shellspy_status_log + shellfilter::log::write $shellspy_status_log "do_shell $shell got '$args' [llength $args]" + set args [do_callback $shell {*}$args] + shellfilter::log::write $shellspy_status_log "do_shell $shell xgot '$args'" + set params [do_callback_parameters $shell] + dict set params -teehandle shellspy + + + set params [dict merge $params [get_channel_config $::testconfig]] + + set id_out [shellfilter::stack::add stdout towindows -action sink-aside-locked -junction 1 -settings {}] + + #shells that take -c and need all args passed together as a string + + set exitinfo [shellfilter::run [concat $shell -c [shellescape $args]] {*}$params] + + shellfilter::stack::remove stdout $id_out + + + if {[lindex $exitinfo 0] eq "exitcode"} { + shellfilter::log::write $shellspy_status_log "do_shell $shell returning $exitinfo" + } + return $exitinfo + } + proc do_wsl {dist args} { + variable shellspy_status_log + shellfilter::log::write $shellspy_status_log "do_wsl $dist got '$args' [llength $args]" + set args [do_callback wsl {*}$args] ;#use dist? + shellfilter::log::write $shellspy_status_log "do_wsl $dist xgot '$args'" + set params [do_callback_parameters wsl] + + dict set params -debug 0 + + + set params [dict merge $params [get_channel_config $::testconfig]] + + + set id_out [shellfilter::stack::add stdout towindows -action sink-aside-locked -junction 1 -settings {}] + + + dict set params -teehandle shellspy ;#shellspyout shellspyerr must exist + set exitinfo [shellfilter::run [concat wsl -d $dist -e [shellescape $args]] {*}$params] + + + shellfilter::stack::remove stdout $id_out + + + if {[lindex $exitinfo 0] eq "exitcode"} { + shellfilter::log::write $shellspy_status_log "do_wsl $dist returning $exitinfo" + } + return $exitinfo + } + + #todo - load these from a callback + set commands [list] + + #shout extension to force use of tclsh as a separate process + #todo - detect various extensions - and use all script-preceding unmatched args as interpreter+options + #e.g perl,php,python etc. + #For tcl will make it easy to test different interpreter output from tclkitsh, tclsh8.6, tclsh8.7 etc + #for now we have a hardcoded default interpreter for tcl as 'tclsh' - todo: get default interps from config + #(or just attempt launch in case there is shebang line in script) + #we may get ambiguity with existing shell match-specs such as -c /c -r. todo - only match those in first slot? + lappend commands [list tclscriptprocess [list match [list .*\.TCL$ .*\.TM$ .*\.TK$] dispatch [list shellspy::do_script_process tclsh %matched%] dispatchtype raw dispatchglobal 1 singleopts {any}]] + for {set i 0} {$i < 25} {incr i} { + lappend commands [list tclscriptprocess [list sub word$i singleopts {any}]] + } + + #camelcase convention .Tcl script before repl + lappend commands [list tclscriptbeforerepl [list match [list .*\.Tcl$ .*\.Tm$ .*\.Tk$ ] dispatch [list shellspy::do_script %matched% "repl_last"] dispatchtype raw dispatchglobal 1 singleopts {any}]] + for {set i 0} {$i < 25} {incr i} { + lappend commands [list tclscriptbeforerepl [list sub word$i singleopts {any}]] + } + + + #Backwards Camelcase convention .tcL - means repl first, script last + lappend commands [list tclscriptafterrepl [list match [list .*\.tcL$ .*\.tM$ .*\.tK$ ] dispatch [list shellspy::do_script %matched% "repl_first"] dispatchtype raw dispatchglobal 1 singleopts {any}]] + for {set i 0} {$i < 25} {incr i} { + lappend commands [list tclscriptafterrepl [list sub word$i singleopts {any}]] + } + + + #we've already handled .Tcl .tcL, .tCl, .TCL - handle any other capitalisations as a script in this process + lappend commands [list tclscript [list match [list .*\.tcl$ .*\.tCL$ .*\.TCl$ .*\.tm$ .*\.tk$ ] dispatch [list shellspy::do_script %matched% "no_repl"] dispatchtype raw dispatchglobal 1 singleopts {any}]] + for {set i 0} {$i < 25} {incr i} { + lappend commands [list tclscript [list sub word$i singleopts {any}]] + } + + + lappend commands [list luascriptprocess [list match [list .*\.lua|Lua|LUA$] dispatch [list shellspy::do_script_process lua %matched% "no_repl"] dispatchtype raw dispatchglobal 1 singleopts {any}]] + for {set i 0} {$i < 25} {incr i} { + lappend commands [list luascriptprocess [list sub word$i singleopts {any}]] + } + + lappend commands [list phpscriptprocess [list match [list .*\.php|Php|PHP$] dispatch [list shellspy::do_script_process php %matched% "no_repl"] dispatchtype raw dispatchglobal 1 singleopts {any}]] + for {set i 0} {$i < 25} {incr i} { + lappend commands [list phpscriptprocess [list sub word$i singleopts {any}]] + } + + lappend commands [list bashraw [list match ^bash$ dispatch [list shellspy::do_shell bash] dispatchtype raw dispatchglobal 1 singleopts {any}]] + for {set i 0} {$i < 25} {incr i} { + lappend commands [list bashraw [list sub word$i singleopts {any}]] + } + lappend commands {shraw {match ^sh$ dispatch {shellspy::do_shell sh} dispatchtype raw dispatchglobal 1 singleopts {any}}} + for {set i 0} {$i < 25} {incr i} { + lappend commands [list shraw [list sub word$i singleopts {any}]] + } + + lappend commands [list runbash [list match ^b$ dispatch [list shellspy::do_shell bash] dispatchtype shell dispatchglobal 1 singleopts {any}]] + for {set i 0} {$i < 25} {incr i} { + lappend commands [list runbash [list sub word$i singleopts {any}]] + } + lappend commands {runsh {match ^s$ dispatch {shellspy::do_shell sh} dispatchtype shell dispatchglobal 1 singleopts {any}}} + for {set i 0} {$i < 25} {incr i} { + lappend commands [list runsh [list sub word$i singleopts {any}]] + } + + lappend commands {runraw {match ^-r$ dispatch shellspy::do_raw dispatchtype raw dispatchglobal 1 singleopts {any}}} + for {set i 0} {$i < 25} {incr i} { + lappend commands [list runraw [list sub word$i singleopts {any}]] + } + lappend commands {runpwsh {match ^-c$ dispatch shellspy::do_in_powershell dispatchtype raw dispatchglobal 1 singleopts {any}}} + for {set i 0} {$i < 25} {incr i} { + lappend commands [list runpwsh [list sub word$i singleopts {any}]] + } + lappend commands {runpwsht {match ^pwsh$ dispatch shellspy::do_in_powershell_terminal dispatchtype raw dispatchglobal 1 singleopts {any}}} + for {set i 0} {$i < 25} {incr i} { + lappend commands [list runpwsht [list sub word$i singleopts {any}]] + } + + + lappend commands {runcmd {match ^/c$ dispatch shellspy::do_in_cmdshell dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}}} + for {set i 0} {$i < 25} {incr i} { + lappend commands [list runcmd [list sub word$i singleopts {any}]] + } + lappend commands {runcmduc {match ^/u/c$ dispatch shellspy::do_in_cmdshelluc dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}}} + for {set i 0} {$i < 25} {incr i} { + lappend commands [list runcmduc [list sub word$i singleopts {any}]] + } + #cmd with bracked args () e.g with vim shellxquote set to "(" + lappend commands [list runcmdb [list match ^cmdb$ dispatch [list shellspy::do_in_cmdshellb %matched%] dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any} pairopts {any}]] + for {set i 0} {$i < 25} {incr i} { + lappend commands [list runcmdb [list sub word$i singleopts {any} longopts {any} pairopts {any}]] + } + + lappend commands [list wslraw [list match ^wsl$ dispatch [list shellspy::do_wsl AlmaLinux9] dispatchtype raw dispatchglobal 1 singleopts {any} longopts {any}]] + for {set i 0} {$i < 25} {incr i} { + lappend commands [list wslraw [list sub word$i singleopts {any}]] + } + + + ############################################################################################ + + #echo raw args to diverted stderr before running the argument analysis + puts -nonewline stderr "exe:[info nameofexecutable] script:[info script] shellspy-rawargs: $::argv\n" + set i 1 + foreach a $::argv { + puts -nonewline stderr "arg$i: '$a'\n" + incr i + } + + + puts stderr "ansi-test:$::shellfilter::ansi::test" + flush stderr + + set argdefinitions [list \ + -caller shellspy_dispatcher \ + -debugargs 0 \ + -debugargsonerror 2 \ + -return all \ + -soloflags {} \ + -defaults [list] \ + -required {none} \ + -extras {all} \ + -commandprocessors $commands \ + -values $::argv ] + + + + if {[catch { + set arglist [check_flags {*}$argdefinitions] + } errMsg]} { + puts -nonewline stderr "|shellspy-stderr> ERROR during command dispatch\n" + puts -nonewline stderr "|shellspy-stderr> $errMsg\n" + puts -nonewline stderr "|shellspy-stderr> [set ::errorInfo]\n" + + shellfilter::log::write $shellspy_status_log "check_flags error: $errMsg" + } else { + shellfilter::log::write $shellspy_status_log "check_flags result: $arglist" + } + + shellfilter::log::write $shellspy_status_log "check_flags dispatch -done-" + + #puts stdout "sp2. $::argv" + + if {[catch { + set tidyinfo [shellfilter::logtidyup] + } errMsg]} { + + shellfilter::log::open shellspy-error {-tag shellspy-error -syslog 127.0.0.1:514} + shellfilter::log::write shellspy-error "logtidyup error $errMsg\n [set ::errorInfo]" + after 200 + } + #don't open more logs.. + #puts stdout ">$tidyinfo" + + + #shellfilter::log::write $shellspy_status_log "logtidyup -done- $tidyinfo" + + set errorlist [dict get $tidyinfo errors] + if {[llength $errorlist]} { + foreach err $errorlist { + puts -nonewline stderr "|shellspy-final> worker-error-set $err\n" + } + } + puts stdout "shellspy -done-" + #shellfilter::log::write $shellspy_status_log "shellspy -done-" + flush stdout + + if {[catch { + shellfilter::logtidyup $shellspy_status_log + } errMsg]} { + shellfilter::log::open shellspy-final {-tag shellspy-final -syslog 127.0.0.1:514} + shellfilter::log::write shellspy-final "FINAL logtidyup error $errMsg\n [set ::errorInfo]" + after 200 + + } + #if we call exit - package require Tk scripts will exit prematurely + #exit 0 +} + +}