Browse Source

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
master
Julian Noble 1 week ago
parent
commit
24a7d50f3b
  1. 418
      src/modules/shellrun-999999.0a1.0.tm
  2. 12
      src/modules/shellrun-buildversion.txt

418
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
}
}

12
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.
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)

Loading…
Cancel
Save