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] |
||||
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