From 24a7d50f3bcf6d22d4e6bb4616d3e47b2c5f41db Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Mon, 6 Jul 2026 12:44:14 +1000 Subject: [PATCH] shellrun 0.1.3: migrate runx + sh_* to punk::args, remove get_run_opts, expand PUNKARGS docs - runx: replaced get_run_opts with punk::args::parse; added PUNKARGS spec with @cmd + -help - runx now honours -debug and --timeout (previously accepted but silently ignored) - sh_run/sh_runout/sh_runerr/sh_runx: migrated from get_run_opts to punk::args::parse with dedicated PUNKARGS specs - sh_* wrappers no longer accept -tcl (was silently swallowed; sh -c always invokes an external shell) - removed get_run_opts proc; all callers now use punk::args - expanded PUNKARGS documentation for run, runconsole, runout, runerr (added @cmd metadata + -help on opts/values) - bumped shellrun-buildversion 0.1.2 -> 0.1.3 Assisted-by: harness=opencode; primary-model=openrouter/z-ai/glm-5.2; api-location=openrouter.ai --- src/modules/shellrun-999999.0a1.0.tm | 418 +++++++++++++++++++------- src/modules/shellrun-buildversion.txt | 12 +- 2 files changed, 321 insertions(+), 109 deletions(-) diff --git a/src/modules/shellrun-999999.0a1.0.tm b/src/modules/shellrun-999999.0a1.0.tm index 3be4a226..406fa6c9 100644 --- a/src/modules/shellrun-999999.0a1.0.tm +++ b/src/modules/shellrun-999999.0a1.0.tm @@ -79,76 +79,45 @@ namespace eval shellrun { - #maintenance: similar used in punk::ns & punk::winrun - #todo - take runopts + aliases as args - #longopts must be passed as a single item ie --timeout=100 not --timeout 100 - proc get_run_opts {arglist} { - if {[catch { - set callerinfo [info level -1] - } errM]} { - set caller "" - } else { - set caller [lindex $callerinfo 0] - } - - #we provide -nonewline even for 'run' even though run doesn't deliver stderr or stdout to the tcl return value - #This is for compatibility with other runX commands, and the difference is also visible when calling from repl. - set known_runopts [list "-echo" "-e" "-nonewline" "-n" "-tcl" "-debug"] - set known_longopts [list "--timeout"] - set known_longopts_msg "" - foreach lng $known_longopts { - append known_longopts_msg "${lng}=val " - } - set aliases [list "-e" "-echo" "-echo" "-echo" "-n" "-nonewline" "-nonewline" "-nonewline" "-tcl" "-tcl" "-debug" "-debug"] ;#include map to self - set runopts [list] - set runoptslong [list] - set cmdargs [list] - - set idx_first_cmdarg [lsearch -not $arglist "-*"] - - set allopts [lrange $arglist 0 $idx_first_cmdarg-1] - set cmdargs [lrange $arglist $idx_first_cmdarg end] - foreach o $allopts { - if {[string match --* $o]} { - lassign [split $o =] flagpart valpart - if {$valpart eq ""} { - error "$caller: longopt $o seems to be missing a value - must be of form --option=value" - } - if {$flagpart ni $known_longopts} { - error "$caller: Unknown runoption $o - known options $known_runopts $known_longopts_msg" - } - lappend runoptslong $flagpart $valpart - } else { - if {$o ni $known_runopts} { - error "$caller: Unknown runoption $o - known options $known_runopts $known_longopts_msg" - } - lappend runopts [dict get $aliases $o] - } - } - return [list runopts $runopts runoptslong $runoptslong cmdargs $cmdargs] - } - - #todo - investigate cause of punk86 run hanging sometimes. An 'after 500' before exit in the called script fixes the issue. punk87 doesn't seem to be affected. lappend PUNKARGS [list { @id -id ::shellrun::run + @cmd -name "shellrun::run"\ + -summary "Run an external command, streaming stdout/stderr to the console in real-time"\ + -help\ + "Run an external command and return the shellfilter exitinfo dict. + + stdout and stderr are written to the console as they arrive; + nothing is captured for the return value. The exitcode is + part of the returned dict — a non-zero exitcode is NOT raised + as a Tcl error, the caller must check it explicitly. + + Use -tcl to run a Tcl script/command instead of an external + process; in that case the returned dict carries a 'result' + key (or error/errorCode/errorInfo on failure) instead of an + exitcode." @leaders -min 0 -max 0 @opts - -nonewline -type none - -tcl -type none -default 0 - -debug -type none -default 0 - --timeout= -type integer + -nonewline -type none -help\ + "Accepted for compatibility with runout/runerr/runx. Has no + effect on run because run does not capture or return stdout." + -tcl -type none -default 0 -help\ + "Interpret cmdname (and cmdarg) as a Tcl command/script rather + than an external process." + -debug -type none -default 0 -help\ + "Enable debug diagnostics from shellfilter::run." + --timeout= -type integer -help\ + "Timeout in milliseconds for the external process." @values -min 1 -max -1 - cmdname -type string - cmdarg -type any -multiple 1 -optional 1 + cmdname -type string -help\ + "Name of the external command (or Tcl command when -tcl is + given)." + cmdarg -type any -multiple 1 -optional 1 -help\ + "Additional arguments passed through to the command." }] proc run {args} { #set_last_run_display [list] - #set splitargs [get_run_opts $args] - #set runopts [dict get $splitargs runopts] - #set runoptslong [dict get $splitargs runoptslong] - #set cmdargs [dict get $splitargs cmdargs] set argd [punk::args::parse $args withid ::shellrun::run] lassign [dict values $argd] leaders opts values received @@ -206,11 +175,25 @@ namespace eval shellrun { lappend PUNKARGS [list { @id -id ::shellrun::runconsole + @cmd -name "shellrun::runconsole"\ + -summary "Run a command via exec with stdout/stderr directed to the console, mimicking Tcl's unknown handler"\ + -help\ + "Resolve cmdname via auto_execok and exec it with stdout + and stderr redirected to the active console. The result is + returned via uplevel with the exec result/options dict, + mirroring the way Tcl's unknown proc handles external + commands. + + Unlike run, this uses exec semantics — there is no + exitcode in the return value and stdout/stderr are not + captured separately." @leaders -min 0 -max 0 @opts @values -min 1 -max -1 - cmdname -type string - cmdarg -type any -multiple 1 -optional 1 + cmdname -type string -help\ + "Name of the command to resolve via auto_execok and run." + cmdarg -type any -multiple 1 -optional 1 -help\ + "Additional arguments passed to the resolved command." }] #run in the way tcl unknown does - but without regard to auto_noexec proc runconsole {args} { @@ -256,16 +239,42 @@ namespace eval shellrun { } lappend PUNKARGS [list { @id -id ::shellrun::runout + @cmd -name "shellrun::runout"\ + -summary "Run an external command and return its stdout"\ + -help\ + "Run an external command, capture stdout and stderr, and + return stdout as the result. stderr and the exitcode are + emitted via repl telemetry chunks but are not part of the + return value. + + By default no console output is produced while the command + runs; use -echo to tee stdout and stderr to the console in + real-time. Use -nonewline to strip a trailing newline from + the returned stdout. + + Use -tcl to run a Tcl script/command instead of an external + process." @leaders -min 0 -max 0 @opts - -echo -type none - -nonewline -type none - -tcl -type none -default 0 - -debug -type none -default 0 - --timeout= -type integer + -echo -type none -help\ + "Tee stdout and stderr to the console in real-time while still + capturing them for the return value." + -nonewline -type none -help\ + "Strip a trailing newline (\\r\\n or \\n) from the returned + stdout." + -tcl -type none -default 0 -help\ + "Interpret cmdname (and cmdarg) as a Tcl command/script rather + than an external process." + -debug -type none -default 0 -help\ + "Enable debug diagnostics from shellfilter::run." + --timeout= -type integer -help\ + "Timeout in milliseconds for the external process." @values -min 1 -max -1 - cmdname -type string - cmdarg -type any -multiple 1 -optional 1 + cmdname -type string -help\ + "Name of the external command (or Tcl command when -tcl is + given)." + cmdarg -type any -multiple 1 -optional 1 -help\ + "Additional arguments passed through to the command." }] proc runout {args} { set argd [punk::args::parse $args withid ::shellrun::runout] @@ -284,11 +293,6 @@ namespace eval shellrun { set runerr "" set RST [a] - #set splitargs [get_run_opts $args] - #set runopts [dict get $splitargs runopts] - #set cmdargs [dict get $splitargs cmdargs] - - #puts stdout "RUNOUT cmdargs: $cmdargs" #todo add -data boolean and -data lastwrite to -settings with default being -data all @@ -422,16 +426,42 @@ namespace eval shellrun { lappend PUNKARGS [list { @id -id ::shellrun::runerr + @cmd -name "shellrun::runerr"\ + -summary "Run an external command and return its stderr"\ + -help\ + "Run an external command, capture stdout and stderr, and + return stderr as the result. stdout and the exitcode are + emitted via repl telemetry chunks but are not part of the + return value. + + By default no console output is produced while the command + runs; use -echo to tee stdout and stderr to the console in + real-time. Use -nonewline to strip a trailing newline from + the returned stderr. + + Use -tcl to run a Tcl script/command instead of an external + process." @leaders -min 0 -max 0 @opts - -echo -type none - -nonewline -type none - -tcl -type none -default 0 - -debug -type none -default 0 - --timeout= -type integer + -echo -type none -help\ + "Tee stdout and stderr to the console in real-time while still + capturing them for the return value." + -nonewline -type none -help\ + "Strip a trailing newline (\\r\\n or \\n) from the returned + stderr." + -tcl -type none -default 0 -help\ + "Interpret cmdname (and cmdarg) as a Tcl command/script rather + than an external process." + -debug -type none -default 0 -help\ + "Enable debug diagnostics from shellfilter::run." + --timeout= -type integer -help\ + "Timeout in milliseconds for the external process." @values -min 1 -max -1 - cmdname -type string - cmdarg -type any -multiple 1 -optional 1 + cmdname -type string -help\ + "Name of the external command (or Tcl command when -tcl is + given)." + cmdarg -type any -multiple 1 -optional 1 -help\ + "Additional arguments passed through to the command." }] proc runerr {args} { set argd [punk::args::parse $args withid ::shellrun::runerr] @@ -449,10 +479,6 @@ namespace eval shellrun { set runout "" set runerr "" - #set splitargs [get_run_opts $args] - #set runopts [dict get $splitargs runopts] - #set cmdargs [dict get $splitargs cmdargs] - set callopts [dict create] if {[dict exists $received "-tcl"]} { dict set callopts -tclscript 1 @@ -567,6 +593,43 @@ namespace eval shellrun { } + lappend PUNKARGS [list { + @id -id ::shellrun::runx + @cmd -name "shellrun::runx"\ + -summary "Run an external command and return a dict of stdout, stderr, and exitcode"\ + -help\ + "Run an external command, capture stdout and stderr, and + return a dict containing stdout, stderr, and exitcode (or + result/error/errorCode/errorInfo for -tcl calls). + + No console output is produced unless -echo is given. Use + -nonewline to strip trailing newlines from captured stdout + and stderr before returning. + + Use -tcl to run a Tcl script/command instead of an external + process." + @leaders -min 0 -max 0 + @opts + -echo -type none -help\ + "Tee stdout and stderr to the console in real-time while still + capturing them for the return dict." + -nonewline -type none -help\ + "Strip a trailing newline (\\r\\n or \\n) from captured stdout + and stderr in the returned dict." + -tcl -type none -default 0 -help\ + "Interpret cmdname (and cmdarg) as a Tcl command/script rather + than an external process." + -debug -type none -default 0 -help\ + "Enable debug diagnostics from shellfilter::run." + --timeout= -type integer -help\ + "Timeout in milliseconds for the external process." + @values -min 1 -max -1 + cmdname -type string -help\ + "Name of the external command (or Tcl command when -tcl is + given)." + cmdarg -type any -multiple 1 -optional 1 -help\ + "Additional arguments passed through to the command." + }] proc runx {args} { #set_last_run_display [list] variable runout @@ -574,11 +637,10 @@ namespace eval shellrun { set runout "" set runerr "" - set splitargs [get_run_opts $args] - set runopts [dict get $splitargs runopts] - set cmdargs [dict get $splitargs cmdargs] + set argd [punk::args::parse $args withid ::shellrun::runx] + lassign [dict values $argd] leaders opts values received - if {"-nonewline" in $runopts} { + if {[dict exists $received "-nonewline"]} { set nonewline 1 } else { set nonewline 0 @@ -586,7 +648,7 @@ namespace eval shellrun { #shellfilter::stack::remove stdout $::repl::id_outstack - if {"-echo" in $runopts} { + if {[dict exists $received "-echo"]} { #float to ensure repl transform doesn't interfere with the output data set stderr_stackid [shellfilter::stack::add stderr tee_to_var -action float -settings {-varname ::shellrun::runerr}] set stdout_stackid [shellfilter::stack::add stdout tee_to_var -action float -settings {-varname ::shellrun::runout}] @@ -601,10 +663,23 @@ namespace eval shellrun { set stdout_stackid [shellfilter::stack::add stdout var -action float-locked -junction 1 -settings {-varname ::shellrun::runout}] } - set callopts "" - if {"-tcl" in $runopts} { - append callopts " -tclscript 1" + set callopts [dict create] + if {[dict exists $received "-tcl"]} { + dict set callopts -tclscript 1 + } + if {[dict exists $received "-debug"]} { + dict set callopts -debug 1 + } + if {[dict exists $received --timeout]} { + dict set callopts -timeout [dict get $opts --timeout] + } + set cmdname [dict get $values cmdname] + if {[dict exists $received cmdarg]} { + set cmdarglist [dict get $values cmdarg] + } else { + set cmdarglist {} } + set cmdargs [concat $cmdname $cmdarglist] #set exitinfo [shellfilter::run $cmdargs -teehandle punksh -inbuffering none -outbuffering none -stdinhandler ::repl::repl_handler] set exitinfo [shellfilter::run $cmdargs {*}$callopts -teehandle punksh -inbuffering none -outbuffering none] @@ -616,7 +691,7 @@ namespace eval shellrun { flush stdout if {[dict exists $exitinfo error]} { - if {"-tcl" in $runopts} { + if {[dict exists $received "-tcl"]} { } else { #todo - check errorInfo makes sense.. return -code? tailcall? @@ -826,30 +901,161 @@ namespace eval shellrun { return $exitinfo } + lappend PUNKARGS [list { + @id -id ::shellrun::sh_run + @cmd -name "shellrun::sh_run"\ + -summary "Run a command line via sh -c, streaming stdout/stderr to the console"\ + -help\ + "Wrapper around shellrun::run that invokes the command line + via sh -c. The entire command line after options is passed + as a single argument to sh -c. + + Accepts the same options as shellrun::run except -tcl, + which is not meaningful for the sh_* wrappers because they + always invoke an external shell." + @leaders -min 0 -max 0 + @opts + -nonewline -type none -help\ + "Accepted for compatibility. Has no effect on run-based + commands because run does not capture output." + -debug -type none -default 0 -help\ + "Enable debug diagnostics from shellfilter::run." + --timeout= -type integer -help\ + "Timeout in milliseconds for the external process." + @values -min 1 -max -1 + cmdargs -type any -multiple 1 -help\ + "Command line to pass to sh -c as a single string argument." + }] proc sh_run {args} { - set splitargs [get_run_opts $args] - set runopts [dict get $splitargs runopts] - set cmdargs [dict get $splitargs cmdargs] + set argd [punk::args::parse $args withid ::shellrun::sh_run] + lassign [dict values $argd] leaders opts values received + set cmdargs [dict get $values cmdargs] + set runopts [list] + if {[dict exists $received -nonewline]} { lappend runopts -nonewline } + if {[dict exists $received -debug]} { lappend runopts -debug } + if {[dict exists $received --timeout]} { lappend runopts --timeout=[dict get $opts --timeout] } #e.g sh -c "ls -l *" #we pass cmdargs to sh -c as a list, not individually tailcall shellrun::run {*}$runopts sh -c $cmdargs } + + lappend PUNKARGS [list { + @id -id ::shellrun::sh_runout + @cmd -name "shellrun::sh_runout"\ + -summary "Run a command line via sh -c and return its stdout"\ + -help\ + "Wrapper around shellrun::runout that invokes the command + line via sh -c. The entire command line after options is + passed as a single argument to sh -c. + + Accepts the same options as shellrun::runout except -tcl, + which is not meaningful for the sh_* wrappers because they + always invoke an external shell." + @leaders -min 0 -max 0 + @opts + -echo -type none -help\ + "Tee stdout and stderr to the console in real-time while still + capturing them for the return value." + -nonewline -type none -help\ + "Strip a trailing newline (\\r\\n or \\n) from the returned + stdout." + -debug -type none -default 0 -help\ + "Enable debug diagnostics from shellfilter::run." + --timeout= -type integer -help\ + "Timeout in milliseconds for the external process." + @values -min 1 -max -1 + cmdargs -type any -multiple 1 -help\ + "Command line to pass to sh -c as a single string argument." + }] proc sh_runout {args} { - set splitargs [get_run_opts $args] - set runopts [dict get $splitargs runopts] - set cmdargs [dict get $splitargs cmdargs] + set argd [punk::args::parse $args withid ::shellrun::sh_runout] + lassign [dict values $argd] leaders opts values received + set cmdargs [dict get $values cmdargs] + set runopts [list] + if {[dict exists $received -echo]} { lappend runopts -echo } + if {[dict exists $received -nonewline]} { lappend runopts -nonewline } + if {[dict exists $received -debug]} { lappend runopts -debug } + if {[dict exists $received --timeout]} { lappend runopts --timeout=[dict get $opts --timeout] } tailcall shellrun::runout {*}$runopts sh -c $cmdargs } + + lappend PUNKARGS [list { + @id -id ::shellrun::sh_runerr + @cmd -name "shellrun::sh_runerr"\ + -summary "Run a command line via sh -c and return its stderr"\ + -help\ + "Wrapper around shellrun::runerr that invokes the command + line via sh -c. The entire command line after options is + passed as a single argument to sh -c. + + Accepts the same options as shellrun::runerr except -tcl, + which is not meaningful for the sh_* wrappers because they + always invoke an external shell." + @leaders -min 0 -max 0 + @opts + -echo -type none -help\ + "Tee stdout and stderr to the console in real-time while still + capturing them for the return value." + -nonewline -type none -help\ + "Strip a trailing newline (\\r\\n or \\n) from the returned + stderr." + -debug -type none -default 0 -help\ + "Enable debug diagnostics from shellfilter::run." + --timeout= -type integer -help\ + "Timeout in milliseconds for the external process." + @values -min 1 -max -1 + cmdargs -type any -multiple 1 -help\ + "Command line to pass to sh -c as a single string argument." + }] proc sh_runerr {args} { - set splitargs [get_run_opts $args] - set runopts [dict get $splitargs runopts] - set cmdargs [dict get $splitargs cmdargs] + set argd [punk::args::parse $args withid ::shellrun::sh_runerr] + lassign [dict values $argd] leaders opts values received + set cmdargs [dict get $values cmdargs] + set runopts [list] + if {[dict exists $received -echo]} { lappend runopts -echo } + if {[dict exists $received -nonewline]} { lappend runopts -nonewline } + if {[dict exists $received -debug]} { lappend runopts -debug } + if {[dict exists $received --timeout]} { lappend runopts --timeout=[dict get $opts --timeout] } tailcall shellrun::runerr {*}$runopts sh -c $cmdargs } + + lappend PUNKARGS [list { + @id -id ::shellrun::sh_runx + @cmd -name "shellrun::sh_runx"\ + -summary "Run a command line via sh -c and return a dict of stdout, stderr, and exitcode"\ + -help\ + "Wrapper around shellrun::runx that invokes the command + line via sh -c. The entire command line after options is + passed as a single argument to sh -c. + + Accepts the same options as shellrun::runx except -tcl, + which is not meaningful for the sh_* wrappers because they + always invoke an external shell." + @leaders -min 0 -max 0 + @opts + -echo -type none -help\ + "Tee stdout and stderr to the console in real-time while still + capturing them for the return dict." + -nonewline -type none -help\ + "Strip a trailing newline (\\r\\n or \\n) from captured stdout + and stderr in the returned dict." + -debug -type none -default 0 -help\ + "Enable debug diagnostics from shellfilter::run." + --timeout= -type integer -help\ + "Timeout in milliseconds for the external process." + @values -min 1 -max -1 + cmdargs -type any -multiple 1 -help\ + "Command line to pass to sh -c as a single string argument." + }] proc sh_runx {args} { - set splitargs [get_run_opts $args] - set runopts [dict get $splitargs runopts] - set cmdargs [dict get $splitargs cmdargs] + set argd [punk::args::parse $args withid ::shellrun::sh_runx] + lassign [dict values $argd] leaders opts values received + set cmdargs [dict get $values cmdargs] + set runopts [list] + if {[dict exists $received -echo]} { lappend runopts -echo } + if {[dict exists $received -nonewline]} { lappend runopts -nonewline } + if {[dict exists $received -debug]} { lappend runopts -debug } + if {[dict exists $received --timeout]} { lappend runopts --timeout=[dict get $opts --timeout] } tailcall shellrun::runx {*}$runopts sh -c $cmdargs } } diff --git a/src/modules/shellrun-buildversion.txt b/src/modules/shellrun-buildversion.txt index c98e34df..8adb77cc 100644 --- a/src/modules/shellrun-buildversion.txt +++ b/src/modules/shellrun-buildversion.txt @@ -1,3 +1,9 @@ -0.1.2 -#First line must be a semantic version number -#all other lines are ignored. \ No newline at end of file +0.1.3 +#First line must be a semantic version number +#all other lines are ignored. +#0.1.3 - migrated runx from get_run_opts to punk::args::parse (added PUNKARGS spec with @cmd + -help) +#0.1.3 - migrated sh_run/sh_runout/sh_runerr/sh_runx from get_run_opts to punk::args::parse (added PUNKARGS specs) +#0.1.3 - removed get_run_opts proc; all callers now use punk::args +#0.1.3 - expanded PUNKARGS documentation for run, runconsole, runout, runerr (added @cmd metadata + -help on opts/values) +#0.1.3 - runx now honours -debug and --timeout (previously accepted but silently ignored) +#0.1.3 - removed -tcl from sh_run/sh_runout/sh_runerr/sh_runx (was silently swallowed with no useful effect; now rejected with usage)