Browse Source
New lean app package src/lib/app-punkscript behind the (previously stub) script subcommand in punk_main.tcl: - `<commands> | <punkexe> script` runs piped stdin to EOF - no trailing exit needed; `<punkexe> script <file> ?args?` runs a file with conventional ::argv0/::argv (bare non-subcommand first arg still reclassifies to the file form). Script may itself read stdin in the file form. - Default punk shell environment via shared definitions (package punk registers deck aliases e.g dev; punk::aliascore::init adds the utility aliases) - no local alias lists, no boilerplate in piped one-liners. - Honest exit codes: 0 success, 1 on error with errorInfo on stderr, script''s own exit honoured. No shellfilter stacks/transforms, no logging side effects, no interactive fallback (terminal stdin with no scriptname is a usage error, never a blocking read or shell). - Stdin form echoes the script''s final result when non-empty (one-shot eval ergonomics: return-value commands like `dev projects.work <glob>` emit their table without a puts wrapper); file form keeps pure script semantics. Verified on both generations (punk902z/tcl9 from _build - bin deploy was file-locked; punksys/tcl8.6 from bin): piped success/error/exit-code, file form with args and stdin passthrough, missing-file error, motivating one-liner `dev projects.work *tomlish*` emitting the project table with exit 0, `src` mode combination (dev modules load), and `shell` subcommand regression smoke. G-015 flipped to achieved 2026-07-07 per GOALS.md rules; goal detail records the verification and the manual console spot-check item. Project version 0.3.0 (new user-visible shell capability - minor bump). Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.commaster
10 changed files with 211 additions and 8 deletions
@ -1,3 +1,3 @@ |
|||||||
[project] |
[project] |
||||||
name = "punkshell" |
name = "punkshell" |
||||||
version = "0.2.7" |
version = "0.3.0" |
||||||
|
|||||||
@ -0,0 +1 @@ |
|||||||
|
package ifneeded app-punkscript 1.0 [list source [file join $dir punkscript.tcl]] |
||||||
@ -0,0 +1,97 @@ |
|||||||
|
package provide app-punkscript 1.0 |
||||||
|
#Lean one-shot script runner for the punk executable 'script' subcommand (goal G-015). |
||||||
|
# |
||||||
|
#Contract (G-015 - see goals/G-015-script-subcommand-piped-stdin.md): |
||||||
|
# - runs a script FILE (first argument, remaining arguments become the script's ::argv) |
||||||
|
# or, with no arguments, the whole of piped/redirected stdin as the script. |
||||||
|
# - the script interp carries the default punk shell module/alias environment |
||||||
|
# (package punk registers the deck aliases such as 'dev'; punk::aliascore |
||||||
|
# provides the utility aliases) so one-liners like 'dev projects.work *x*' |
||||||
|
# need no package require boilerplate. |
||||||
|
# - NO shellfilter channel stacks/transforms, no logging side effects, no repl, |
||||||
|
# and no interactive fallback under any eof/error condition. This package is |
||||||
|
# deliberately isolated from app-punkshell (share definitions via modules, |
||||||
|
# never launcher control flow). |
||||||
|
# - the launch plumbing emits nothing on stdout/stderr; only the script's own |
||||||
|
# output (and, on error, the error report to stderr) appears. |
||||||
|
# - exit codes: 0 on success, 1 on script error or usage error; a script's own |
||||||
|
# explicit exit code is honoured. |
||||||
|
# |
||||||
|
#punk_main.tcl sets ::argv to the arguments after the 'script' subcommand |
||||||
|
#before requiring this package (a bare non-subcommand first argument is also |
||||||
|
#reclassified as 'script <arg...>'). |
||||||
|
|
||||||
|
apply {{} { |
||||||
|
#stdin-is-a-terminal detection, self-contained (no punkboot dependency so |
||||||
|
#this package can also host in plain tclsh contexts). |
||||||
|
#Windows consoles expose -inputmode on stdin; unix terminals expose -mode. |
||||||
|
set stdin_is_terminal [apply {{} { |
||||||
|
if {[catch {chan configure stdin} conf]} { |
||||||
|
return 0 ;#no usable stdin at all - treat as non-terminal |
||||||
|
} |
||||||
|
expr {[dict exists $conf -inputmode] || [dict exists $conf -mode]} |
||||||
|
}}] |
||||||
|
|
||||||
|
#default punk shell environment via shared definitions - no local alias lists |
||||||
|
package require punk |
||||||
|
package require punk::aliascore |
||||||
|
punk::aliascore::init |
||||||
|
|
||||||
|
set arglist $::argv |
||||||
|
if {[llength $arglist]} { |
||||||
|
#file form: scriptname ?args...? |
||||||
|
#works with or without piped stdin present - the script may read stdin itself |
||||||
|
set scriptname [lindex $arglist 0] |
||||||
|
set scriptargs [lrange $arglist 1 end] |
||||||
|
set scriptpath [file normalize $scriptname] |
||||||
|
if {![file exists $scriptpath]} { |
||||||
|
puts stderr "punk script: script file not found: '$scriptpath'" |
||||||
|
exit 1 |
||||||
|
} |
||||||
|
set ::argv0 $scriptpath |
||||||
|
set ::argv $scriptargs |
||||||
|
set ::argc [llength $scriptargs] |
||||||
|
set ::tcl_interactive 0 |
||||||
|
info script $scriptpath |
||||||
|
if {[catch {uplevel #0 [list source $scriptpath]}]} { |
||||||
|
puts stderr $::errorInfo |
||||||
|
flush stderr |
||||||
|
exit 1 |
||||||
|
} |
||||||
|
flush stdout |
||||||
|
flush stderr |
||||||
|
exit 0 |
||||||
|
} else { |
||||||
|
#stdin form: read the whole of piped/redirected stdin as the script. |
||||||
|
#An interactive terminal with no scriptname is a usage error - never |
||||||
|
#block on a console read and never fall through to a shell. |
||||||
|
if {$stdin_is_terminal} { |
||||||
|
puts stderr "punk script: no script file specified and stdin is a terminal" |
||||||
|
puts stderr "usage: <punkexe> script <scriptfile> ?args...?" |
||||||
|
puts stderr " or: <commands> | <punkexe> script" |
||||||
|
exit 1 |
||||||
|
} |
||||||
|
set script [read stdin] |
||||||
|
set ::argv0 "-" |
||||||
|
set ::argv {} |
||||||
|
set ::argc 0 |
||||||
|
set ::tcl_interactive 0 |
||||||
|
if {[catch {uplevel #0 $script} result]} { |
||||||
|
puts stderr $::errorInfo |
||||||
|
flush stderr |
||||||
|
exit 1 |
||||||
|
} |
||||||
|
#one-shot eval ergonomics for the stdin form only: the script's value |
||||||
|
#(its final command's result) is written to stdout when non-empty, so |
||||||
|
#one-liners whose commands RETURN their output (e.g 'dev projects.work |
||||||
|
#<glob>' - displayed by the repl's result display in a shell) emit it |
||||||
|
#here too without needing a puts wrapper. File-form scripts keep pure |
||||||
|
#script semantics (results discarded). |
||||||
|
if {[string length $result]} { |
||||||
|
puts stdout $result |
||||||
|
} |
||||||
|
flush stdout |
||||||
|
flush stderr |
||||||
|
exit 0 |
||||||
|
} |
||||||
|
}} |
||||||
@ -0,0 +1 @@ |
|||||||
|
package ifneeded app-punkscript 1.0 [list source [file join $dir punkscript.tcl]] |
||||||
@ -0,0 +1,97 @@ |
|||||||
|
package provide app-punkscript 1.0 |
||||||
|
#Lean one-shot script runner for the punk executable 'script' subcommand (goal G-015). |
||||||
|
# |
||||||
|
#Contract (G-015 - see goals/G-015-script-subcommand-piped-stdin.md): |
||||||
|
# - runs a script FILE (first argument, remaining arguments become the script's ::argv) |
||||||
|
# or, with no arguments, the whole of piped/redirected stdin as the script. |
||||||
|
# - the script interp carries the default punk shell module/alias environment |
||||||
|
# (package punk registers the deck aliases such as 'dev'; punk::aliascore |
||||||
|
# provides the utility aliases) so one-liners like 'dev projects.work *x*' |
||||||
|
# need no package require boilerplate. |
||||||
|
# - NO shellfilter channel stacks/transforms, no logging side effects, no repl, |
||||||
|
# and no interactive fallback under any eof/error condition. This package is |
||||||
|
# deliberately isolated from app-punkshell (share definitions via modules, |
||||||
|
# never launcher control flow). |
||||||
|
# - the launch plumbing emits nothing on stdout/stderr; only the script's own |
||||||
|
# output (and, on error, the error report to stderr) appears. |
||||||
|
# - exit codes: 0 on success, 1 on script error or usage error; a script's own |
||||||
|
# explicit exit code is honoured. |
||||||
|
# |
||||||
|
#punk_main.tcl sets ::argv to the arguments after the 'script' subcommand |
||||||
|
#before requiring this package (a bare non-subcommand first argument is also |
||||||
|
#reclassified as 'script <arg...>'). |
||||||
|
|
||||||
|
apply {{} { |
||||||
|
#stdin-is-a-terminal detection, self-contained (no punkboot dependency so |
||||||
|
#this package can also host in plain tclsh contexts). |
||||||
|
#Windows consoles expose -inputmode on stdin; unix terminals expose -mode. |
||||||
|
set stdin_is_terminal [apply {{} { |
||||||
|
if {[catch {chan configure stdin} conf]} { |
||||||
|
return 0 ;#no usable stdin at all - treat as non-terminal |
||||||
|
} |
||||||
|
expr {[dict exists $conf -inputmode] || [dict exists $conf -mode]} |
||||||
|
}}] |
||||||
|
|
||||||
|
#default punk shell environment via shared definitions - no local alias lists |
||||||
|
package require punk |
||||||
|
package require punk::aliascore |
||||||
|
punk::aliascore::init |
||||||
|
|
||||||
|
set arglist $::argv |
||||||
|
if {[llength $arglist]} { |
||||||
|
#file form: scriptname ?args...? |
||||||
|
#works with or without piped stdin present - the script may read stdin itself |
||||||
|
set scriptname [lindex $arglist 0] |
||||||
|
set scriptargs [lrange $arglist 1 end] |
||||||
|
set scriptpath [file normalize $scriptname] |
||||||
|
if {![file exists $scriptpath]} { |
||||||
|
puts stderr "punk script: script file not found: '$scriptpath'" |
||||||
|
exit 1 |
||||||
|
} |
||||||
|
set ::argv0 $scriptpath |
||||||
|
set ::argv $scriptargs |
||||||
|
set ::argc [llength $scriptargs] |
||||||
|
set ::tcl_interactive 0 |
||||||
|
info script $scriptpath |
||||||
|
if {[catch {uplevel #0 [list source $scriptpath]}]} { |
||||||
|
puts stderr $::errorInfo |
||||||
|
flush stderr |
||||||
|
exit 1 |
||||||
|
} |
||||||
|
flush stdout |
||||||
|
flush stderr |
||||||
|
exit 0 |
||||||
|
} else { |
||||||
|
#stdin form: read the whole of piped/redirected stdin as the script. |
||||||
|
#An interactive terminal with no scriptname is a usage error - never |
||||||
|
#block on a console read and never fall through to a shell. |
||||||
|
if {$stdin_is_terminal} { |
||||||
|
puts stderr "punk script: no script file specified and stdin is a terminal" |
||||||
|
puts stderr "usage: <punkexe> script <scriptfile> ?args...?" |
||||||
|
puts stderr " or: <commands> | <punkexe> script" |
||||||
|
exit 1 |
||||||
|
} |
||||||
|
set script [read stdin] |
||||||
|
set ::argv0 "-" |
||||||
|
set ::argv {} |
||||||
|
set ::argc 0 |
||||||
|
set ::tcl_interactive 0 |
||||||
|
if {[catch {uplevel #0 $script} result]} { |
||||||
|
puts stderr $::errorInfo |
||||||
|
flush stderr |
||||||
|
exit 1 |
||||||
|
} |
||||||
|
#one-shot eval ergonomics for the stdin form only: the script's value |
||||||
|
#(its final command's result) is written to stdout when non-empty, so |
||||||
|
#one-liners whose commands RETURN their output (e.g 'dev projects.work |
||||||
|
#<glob>' - displayed by the repl's result display in a shell) emit it |
||||||
|
#here too without needing a puts wrapper. File-form scripts keep pure |
||||||
|
#script semantics (results discarded). |
||||||
|
if {[string length $result]} { |
||||||
|
puts stdout $result |
||||||
|
} |
||||||
|
flush stdout |
||||||
|
flush stderr |
||||||
|
exit 0 |
||||||
|
} |
||||||
|
}} |
||||||
Loading…
Reference in new issue