$pscript] {uplevel 1 [concat $rhs $segment_members_filled [
]]}]
#set snew "set pipe_$i \[uplevel 1 \[list $rhs $segment_members_filled "
#append snew "set pipe_[expr $i -1]"
#append pscript $snew
set pscript [funcl::o_of_n 1 [list $rhs {*}$segment_members] $pscript]
}
}
set cmdline_result [uplevel 1 [concat $rhs $segment_members_filled]]
set d [_multi_bind_result $returnvarspec [punk::K $cmdline_result [unset cmdline_result]]]
#multi_bind_result needs to return a funcl for rhs of:
#lindex [list [set syncvar [main pipeline.. ]] [rhs binding funcl...] 1 ]
#which uses syncvar
#
#The lhs of 'list' runs first so now syncvar can be the root level of the rhs function list and bind the necessary vars.
#NOTE: unintuitively, we are returning the value of rhs to the main pipleline! (leftmost binding) this is because the leftmost binding determines what goes back to the pipeline result
set r [_handle_bind_result $d]
set segment_result $r
}
#the subresult doesn't need to go backwards - as the final assignment can emit the result into a variable
#It makes more sense and is ultimately more useful (and more easy to reason about) for the result of each assignment to be related only to the pre-pipe section
#It may however make a good debug point
#puts stderr "segment $i segment_result:$segment_result"
#examine tailremaining.
# either x x x |?> y y y ...
# or just y y y
#we want the x side for next loop
#set up the conditions for the next loop
#|> x=y args
# inpipespec - contents of previous piper |xxx>
# outpipespec - empty or content of subsequent piper |xxx>
# previous_result
# assignment (x=y)
set pipespec($j,in) $pipespec($i,out)
set outpipespec ""
set tailmap ""
set next_pipe_posn -1
if {[llength $tailremaining]} {
#set tailmap [lmap v $tailremaining {lreplace [split $v {}] 1 end-1}]
##e.g for: a b c |> e f g |> h
#set next_pipe_posn [lsearch $tailmap {| >}]
set next_pipe_posn [lsearch $tailremaining "|*>"]
set outpipespec [string range [lindex $tailremaining $next_pipe_posn] 1 end-1]
}
set pipespec($j,out) $outpipespec
set segment_members_script_index [list]
set script_like_first_word 0
if {[llength $tailremaining] || $next_pipe_posn >= 0} {
if {$next_pipe_posn >=0} {
set segment_members [lrange $tailremaining 0 $next_pipe_posn-1] ;#exclude only piper |xxx>
set tailremaining [lrange $tailremaining $next_pipe_posn+1 end]
} else {
set segment_members $tailremaining
set tailremaining [list]
}
#assignment is the arg immediately following |> operator e.g x.=blah or x=etc (or a normal commandlist or script!)
set segment_first_word ""
set returnvarspec "" ;# the lhs of x=y
set segment_op ""
set rhs ""
if {[llength $segment_members]} {
if {[arg_is_script_shaped [lindex $segment_members 0]]} {
set segment_first_word [lindex $segment_members 0]
set segment_second_word [lindex $segment_members 1]
set segment_members_script_index 0
set segment_op ""
} else {
set possible_assignment [lindex $segment_members 0]
if {[regexp $re_dot_assign $possible_assignment _ returnvarspec rhs]} {
set segment_op ".="
if {![string length $rhs]} {
set segment_first_word [lindex $segment_members 1]
set segment_second_word [lindex $segment_members 2]
set script_like_first_word [arg_is_script_shaped $segment_first_word]
if {$script_like_first_word} {
set segment_members_script_index 1
}
} else {
set segment_first_word $rhs
set segment_second_word [lindex $segment_members 1]
}
} elseif {[regexp $re_assign $possible_assignment _ returnvarspec rhs]} {
set segment_op "="
#never scripts
set segment_first_word [lindex $segment_members 1]
set segment_second_word [lindex $segment_members 2]
} else {
#no assignment operator and not script shaped
set segment_op ""
set returnvarspec ""
set segment_first_word [lindex $segment_members 0]
set segment_first_word [lindex $segment_members 1]
#puts stderr ">>3 no-operator segment_first_word: '$segment_first_word'"
}
}
} else {
#?? two pipes in a row ?
debug.punk.pipe {[a+ yellow bold]WARNING: no segment members found[a+]} 0
set segment_members return
set segment_first_word return
}
#set forward_result $segment_result
set previous_result $segment_result
} else {
debug.punk.pipe {[a+ cyan bold]End of pipe segments ($i)[a+]} 4
set more_pipe_segments 0
}
#the segment_result is based on the leftmost var on the lhs of the .=
#whereas forward_result is always the entire output of the segment
lappend segment_result_list $segment_result
incr i
incr j
} ;# end while
return [lindex $segment_result_list end]
#return $forward_result
}
proc configure_unknown {} {
#-----------------------------
#these are critical e.g core behaviour or important for repl displaying output correctly
#----------------
#for var="val {a b c}"
#proc ::punk::val {{v {}}} {tailcall lindex $v}
proc ::punk::val {{v {}}} {return $v} ;#2023 - approx 2x faster than the tailcall lindex version
#----------------
#can't use know - because we don't want to return before original unknown body is called.
proc ::unknown {args} [string map [list] {
set ::punk::last_run_display [list]
set ::repl::last_unknown [lindex $args 0] ;#jn
}][info body ::unknown]
#handle process return dict of form {exitcode num etc blah}
#ie when the return result as a whole is treated as a command
#exitcode must be the first key
know {[lindex $args 0 0] eq "exitcode"} {
uplevel 1 [list exitcode {*}[lrange [lindex $args 0] 1 end]]
}
#-----------------------------
#
# potentially can be disabled by config(?) - but then scripts not able to use all repl features..
know {[expr $args] || 1} {
#todo - repl output info that it was evaluated as an expression
expr $args
}
#it is significantly faster to call a proc like this than to inline it in the unknown proc
proc ::punk::range {from to args} {
set count [expr {($to -$from) + 1}]
incr from -1
return [lmap v [lrepeat $count 0] {incr from}]
}
know {[regexp {^([0-9]+)\.\.([0-9]+)$} [lindex $args 0 0] -> from to]} {
punk::range $from $to
}
#if {[info proc ::punk::_unknown] eq ""} {rename unknown ::punk::_unknown}
proc ::punk::_unknown_assign_dispatch {partzerozero varspecs rhs arglist} {
set tail [lassign $args hd]
if {$hd ne $partzerozero} {
regexp $punk::re_assign $hd _ varspecs rhs
}
tailcall ::punk::match_assign $varspecs $rhs $tail
}
#variable re_assign {^([^\r\n=\{]*)=(.*)}
know {[regexp $punk::re_assign [lindex $args 0 0] partzerozero varspecs rhs]} {
#if {![string length $varspecs]} {
#todo allow = with novar and just return value
#error "usage varspecs=val varspecs cannot be empty string using this syntax. Use ''set {} val' if you want to set a var with an empty-string name"
#}
#characters directly following = need to be assigned to the var even if they are escaped whitespace (e.g \t \r \n)
#unescaped whitespace causes the remaining elements to be part of the tail -ie are appended to the var as a list
#e.g x=a\nb c
#x will be assigned the list {a\nb c} ie the spaces between b & c are not maintained
set tail [lassign $args hd]
if {$hd ne $partzerozero} {
regexp $punk::re_assign $hd _ varspecs rhs
}
#must be tailcall so match_assign runs at same level as the unknown proc
tailcall ::punk::match_assign $varspecs $rhs $tail
}
#ensure == is after = in know sequence
#.* on left is pretty broad - todo: make it a little more specific to avoid unexpected interactions
know {[regexp {(.*)==(.*)} [lindex $args 0] _ val1 val2]} {
if {![string length [string trim $val2]]} {
if {[llength $args] > 1} {
#error "Extra args after comparison operator ==. usage e.g : \$var1==\$var2 or \$var1==\$var2 + 2"
set val2 [string cat {*}[lrange $args 1 end]]
return [expr {$val1 eq $val2}]
}
return $val1
} elseif {[llength $args] == 1} {
#simple comparison
if {[string is digit -strict $val1$val2]} {
return [expr {$val1 == $val2}]
} else {
return [string equal $val1 $val2]
}
} elseif {![catch {expr $val2 {*}[lrange $args 1 end]} evaluated]} {
if {[string is digit -strict $val1$evaluated]} {
return [expr {$val1 == $evaluated}]
} else {
return [expr {$val1 eq $evaluated}]
}
} else {
set evaluated [uplevel 1 [list {*}$val2 {*}[lrange $args 1 end]]]
if {[string is digit -strict $val1$evaluated]} {
return [expr {$val1 == $evaluated}]
} else {
return [expr {$val1 eq $evaluated}]
}
}
}
#.= must come after = here to ensure it comes before = in the 'unknown' proc
#set punk::re_dot_assign {([^=]*)\.=(.*)}
#know {[regexp $punk::re_dot_assign [lindex $args 0 0] _ varspecs rhs]} {
# set tail [expr {([lindex $args 0] eq [lindex $args 0 0]) ? [lrange $args 1 end] : [concat [lrange [lindex $args 0] 1 end] [lrange $args 1 end] ] }]
# tailcall ::punk::match_exec $varspecs $rhs {*}$tail
# #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]]
#}
know {[regexp $punk::re_dot_assign [lindex $args 0 0] partzerozero varspecs rhs]} {
set argstail [lassign $args hd]
#set tail [expr {($hd eq $partzerozero) ? $argstail : [concat [lrange $hd 1 end] $argstail ] }] ;#!WRONG. expr will convert some numbers to scientific notation - this is premature/undesirable!
#avoid using the return from expr and it works:
expr {($hd eq $partzerozero) ? [set tail $argstail] : [set tail [concat [lrange $hd 1 end] $argstail ]] }
tailcall ::punk::match_exec $varspecs $rhs {*}$tail
#return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]]
}
#know {[regexp {^=([^=]*)} [lindex $args 0] _ v1]} {
# set calc [concat $v1 [lrange $args 1 end]]
# puts stderr "= $calc"
# return [expr $calc]
#}
}
configure_unknown
#if client redefines 'unknown' after package require punk, they must call punk::configure_unknown afterwards.
#
#main Pipe initiator function - needed especially if 'unknown' not configured to interpret x.= x= etc
#Should theoretically be slightly faster.. but pipelines are relatively slow until we can get pipeline compiling and optimisation.
proc % {args} {
set arglist [lassign $args assign] ;#tail, head
if {$assign eq ".="} {
set cmdlist [list ::punk::match_exec "" "" {*}$arglist]
} elseif {$assign eq "="} {
set cmdlist [list ::punk::match_assign "" "" $arglist]
} elseif {![punk::arg_is_script_shaped $assign] && [string index $assign end] eq "="} {
set re_equals {^([^ \t\r\n=\{]*)=$}
set re_dotequals {^([^ \t\r\n=\{]*)\.=$}
if {[regexp $re_dotequals $assign _ returnvarspecs]} {
set cmdlist [list ::punk::match_exec $returnvarspecs "" {*}$arglist]
} elseif {[regexp $re_equals $assign _ returnvarspecs]} {
set cmdlist [list ::punk::match_assign $returnvarspecs "" $arglist]
} else {
error "pipesyntax punk::% unable to interpret pipeline '$args'"
}
} else {
set cmdlist [list ::punk::match_exec "" "" {*}$args]
}
tailcall {*}$cmdlist
}
proc ispipematch {args} {
expr {[lindex [uplevel 1 [list pipematch {*}$args]] 0] eq "ok"}
}
#pipe initiator which will never raise an error *except for pipesyntax* , but always returns {ok {result something}} or {error {mismatch something}} or, for tcl errors {error {reason something}}
proc pipematch {args} {
#debug.punk.pipe {pipematch level [info level] levelinfo [info level 0]} 2
variable re_dot_assign
variable re_assign
set arglist [lassign $args assign]
if {$assign eq ".="} {
set cmdlist [list ::punk::match_exec "" "" {*}$arglist]
} elseif {$assign eq "="} {
set cmdlist [list ::punk::match_assign "" "" $arglist]
} elseif {[regexp $re_dot_assign $assign _ returnvarspecs rhs]} {
set cmdlist [list ::punk::match_exec $returnvarspecs $rhs {*}$arglist]
} elseif {[regexp $re_assign $assign _ returnvarspecs rhs]} {
set cmdlist [list ::punk::match_assign $returnvarspecs $rhs $arglist]
} else {
set cmdlist $args
#return [dict create error [dict create reason [dict create pipematch bad_first_word value $assign pipeline [list pipematch $assign {*}$args]]]]
}
if {[catch {uplevel 1 $cmdlist} result]} {
#debug.punk.pipe {pipematch error $result} 4
if {[string match "binding*mismatch*" $result]} {
#error {reason xxx} should only be returned for underlying tcl errors. error {someotherkey xxx} for structured errors such as a binding mismatch
#return [dict create error [dict create mismatch $result]]
return [list error [list mismatch $result]]
}
if {[string match "pipesyntax*" $result]} {
error $result
}
#return [dict create error [dict create reason $result]]
return [list error [list reason $result]]
} else {
#debug.punk.pipe {pipematch result $result } 4
#return [dict create ok [dict create result $result]]
return [list ok [list result $result]]
}
}
proc pipenomatchvar {varname args} {
if {[string first = $varname] >=0} {
#first word "pipesyntax" is looked for by pipecase
error "pipesyntax pipenomatch expects a simple varname as first argument"
}
#debug.punk.pipe {pipematch level [info level] levelinfo [info level 0]} 2
set assign [lindex $args 0]
set arglist [lrange $args 1 end]
if {[string first = $assign] >= 0} {
variable re_dot_assign
variable re_assign
#what if we get passed a script block containing = ?? e.g {error x=a}
if {$assign eq ".="} {
set cmdlist [list ::punk::match_exec "" "" {*}$arglist]
} elseif {$assign eq "="} {
set cmdlist [list ::punk::match_assign "" "" $arglist]
} elseif {[regexp $re_dot_assign $assign _ returnvarspecs rhs]} {
set cmdlist [list ::punk::match_exec $returnvarspecs $rhs {*}$arglist]
} elseif {[regexp $re_assign $assign _ returnvarspecs rhs]} {
set cmdlist [list ::punk::match_assign $returnvarspecs $rhs $arglist]
} else {
debug.punk.pipe {[a+ yellow bold] Unexpected arg following pipenomatchvar variable [a+]} 0
set cmdlist $args
#return [dict create error [dict create reason [dict create pipematch bad_first_word value $assign pipeline [list pipematch $assign {*}$args]]]]
}
} else {
set cmdlist $args
}
upvar 1 $varname nomatchvar
if {[catch {uplevel 1 $cmdlist} result]} {
debug.punk.pipe {[a+ yellow bold]pipematchnomatch error $result[a+]} 3
if {[string match "pipesyntax*" $result]} {
set errordict [dict create error [dict create pipesyntax $result]]
set nomatchvar $errordict
error $result
}
if {[string match "binding*mismatch*" $result]} {
#error {reason xxx} should only be returned for underlying tcl errors. error {someotherkey xxx} for structured errors such as a binding mismatch
set errordict [dict create error [dict create mismatch $result]]
set nomatchvar $errordict
error $result
}
set errordict [dict create error [dict create reason $result]]
set nomatchvar $errordict
#re-raise the error for pipeswitch to deal with
error $result
} else {
debug.punk.pipe {pipematchnomatch result $result } 4
set nomatchvar ""
#uplevel 1 [list set $varname ""]
#return raw result only - to pass through to pipeswitch
return $result
#return [dict create ok [dict create result $result]]
}
}
#should only raise an error for pipe syntax errors - all other errors should be wrapped
proc pipecase {args} {
#debug.punk.pipe {pipecase level [info level] levelinfo [info level 0]} 9
set arglist [lassign $args assign]
if {$assign eq ".="} {
set cmdlist [list ::punk::match_exec "" "" {*}$arglist]
} elseif {$assign eq "="} {
set cmdlist [list ::punk::match_assign "" "" $arglist]
} elseif {![punk::arg_is_script_shaped $assign] && [string index $assign end] eq "="} {
set re_equals {^([^ \t\r\n=\{]*)=$}
set re_dotequals {^([^ \t\r\n=\{]*)\.=$}
if {[regexp $re_dotequals $assign _ returnvarspecs]} {
set cmdlist [list ::punk::match_exec $returnvarspecs "" {*}$arglist]
} elseif {[regexp $re_equals $assign _ returnvarspecs]} {
set cmdlist [list ::punk::match_assign $returnvarspecs "" $arglist]
} else {
error "pipesyntax punk::% unable to interpret pipeline '$args'"
}
} else {
set cmdlist [list ::punk::match_exec "" "" {*}$args]
}
if {[catch {uplevel 1 $cmdlist} result]} {
#puts stderr "====>>> $result"
if {[string match "pipesyntax*" $result]} {
error $result
}
if {[string match "binding*mismatch*" $result]} {
#error {reason xxx} should only be returned for underlying tcl errors. error {someotherkey xxx} for structured errors such as a binding mismatch
return [dict create error [dict create mismatch $result]]
#return [dict create error [dict create reason $result]]
}
#we can't always treat $result as a list - may be malformed
if {[catch {lindex $result 0} word1]} {
tailcall error $result
} else {
if {$word1 in [list "switcherror" "funerror"]} {
error $result "pipecase [lsearch -all -inline $args "*="]"
}
if {$word1 in [list "resultswitcherror" "resultfunerror"]} {
#recast the error as a result without @@ok wrapping
#use the tailcall return to stop processing other cases in the switch!
tailcall return [dict create error $result]
}
if {$word1 eq "ignore"} {
#suppress error, but use normal return
return [dict create error [dict create suppressed $result]]
} else {
#normal tcl error
#return [dict create error [dict create reason $result]]
tailcall error $result
}
}
} else {
tailcall return [dict create ok [dict create result $result]]
}
}
#note that pipeswitch deliberately runs in callers scope to have direct access to variables - it is akin to a control structure.
#It also - somewhat unusually accepts args - which we provide as 'switchargs'
#This is unorthodox/risky in that it will clobber any existing var of that name in callers scope.
#Solve using documentation.. consider raising error if 'switchargs' already exists, which would require user to unset switchargs in some circumstances.
proc pipeswitch {pipescript args} {
#set nextargs $args
#unset args
#upvar args upargs
#set upargs $nextargs
upvar switchargs switchargs
set switchargs $args
uplevel 1 [list if 1 $pipescript]
}
proc ansi+ {args} {
variable ansi_disabled
if {$ansi_disabled == 1} {
return
}
tailcall ::shellfilter::ansi::+ {*}$args
}
proc ansi {{onoff {}}} {
variable ansi_disabled
if {[string length $onoff]} {
set onoff [string tolower $onoff]
if {$onoff in [list 1 on true yes]} {
interp alias "" a+ "" punk::ansi+
set ansi_disabled 0
} elseif {$onoff in [list 0 off false no]} {
interp alias "" a+ "" control::no-op
set ansi_disabled 1
} else {
error "punk::ansi expected 0|1|on|off|true|false|yes|no"
}
}
catch {repl::reset_prompt}
return [expr {!$ansi_disabled}]
}
proc scriptlibpath {{shortname {}} args} {
upvar ::punk::config::running running_config
set scriptlib [dict get $running_config scriptlib]
if {[string match "lib::*" $shortname]} {
set relpath [string map [list "lib::" "" "::" "/"] $shortname]
set relpath [string trimleft $relpath "/"]
set fullpath $scriptlib/$relpath
} else {
set shortname [string trimleft $shortname "/"]
set fullpath $scriptlib/$shortname
}
return $fullpath
}
#todo - something better - 'previous' rather than reverting to startup
proc channelcolors {{onoff {}}} {
upvar ::punk::config::running running_config
upvar ::punk::config::startup startup_config
if {![string length $onoff]} {
return [list stdout [dict get $running_config color_stdout] stderr [dict get $running_config color_stderr]]
} else {
set lower_onoff [string tolower $onoff]
if {$lower_onoff in [list true on 1]} {
dict set running_config color_stdout [dict get $startup_config color_stdout]
dict set running_config color_stderr [dict get $startup_config color_stderr]
} elseif {$lower_onoff in [list false off 0]} {
dict set running_config color_stdout ""
dict set running_config color_stderr ""
} else {
error "channelcolors: invalid value $onoff - expected true|false|on|off|1|0"
}
}
return [list stdout [dict get $running_config color_stdout] stderr [dict get $running_config color_stderr]]
}
#useful for aliases e.g treemore -> xmore tree
proc xmore {args} {
if {[llength $args]} {
{*}$args | more
} else {
error "usage: punk::xmore args where args are run as {*}\$args | more"
}
}
proc winpath {path} {
#NOTE: tcl file exists gives different answers on windows for paths like /c depending on cwd (presumably based on file pathtype of volumerelative)
#This is add odds with attempting to navigate on a windows system which has cygwin, wsl etc... It also makes it difficult for functions intended to operate independent of CWD.
#e.g there is potential confusion when there is a c folder on c: drive (c:/c)
#I will attempt to provide a coherent operation for winpath ./ ../ etc , but it may disallow for example; change to /something or /x where these don't match a driveletter or /mnt
#whereas tcl may allow cd to /something if a something folder happens to exist on the current volume based on cwd.
#I think it's preferable to require an explicit driveletter /x or /mnt when using unix-like paths on windows - but practical considerations may prove me wrong..
#It's possible that this function should also ignore the current set of driveletters - and operate completely independent of whether a path actually exists
#This makes it hard to use things like 'file normalize' - which also looks at things like current volume.
#
#Note for example the results of 'which' grep on windows can produce a path like /c/Users/somewhere/bin/grep
#which tcl's file normalize may change to C:/c/Users or X:/c/Users - based on current volumen. Given that C:/c might exist - this can be problematic in a couple of ways.
#The mixing of unix-like and windows commands on the same machine is a large part of the problem.. but this mix is now common
#
#convert /c/etc to C:/etc
set re_slash_x_slash {^/([[:alpha:]]){1}/.*}
set re_slash_else {^/([[:alpha:]]*)(.*)}
set volumes [file volumes]
#exclude things like //zipfs:/
set driveletters [list]
foreach v $volumes {
if {[regexp {^([[:alpha:]]){1}:/$} $v _ letter]} {
lappend driveletters $letter
}
}
#puts stderr "->$driveletters"
if {[regexp $re_slash_x_slash $path _ letter]} {
#upper case appears to be windows canonical form
set path [string toupper $letter]:/[string range $path 3 end]
} elseif {[regexp {^/mnt|MNT/([[:alpha:]]){1}/.*} $path _ letter]} {
set path [string toupper $letter]:/[string range $path 7 end]
} elseif {[regexp $re_slash_else $path _ firstpart remainder]} {
#could be for example /c or /something/users
if {[string length $firstpart] == 1} {
set letter $firstpart
set path [string toupper $letter]:/
} else {
#attempt to use cygpath helper
if {![catch {
set cygpath [runout -n cygpath -w $path] ;#!
set ::punk::last_run_display [list] ;#hack - review shouldn't really be necessary.. but because we call winpath from ./ - the repl looks for last_run_display
} errM]} {
set path [string map [list "\\" "/"] $cygpath]
} else {
error "Path '$path' does not appear to be in a standard form. For unix-like paths on windows such as /x, x must correspond to a drive letter. Consider installing cygwin's cygpath tool to see if that helps."
}
}
}
#puts stderr "=> $path"
#things like 'which' seem to return a path minus the .exe - so we'll just test the containing folder
#
#By now file normalize shouldn't do too many shannanigans related to cwd..
#We want it to look at cwd for relative paths.. but we don't consider things like /c/Users to be relative even on windows
if {![file exists [file dirname $path]]} {
set path [file normalize $path]
#may still not exist.. that's ok.
}
return $path
}
proc windir {path} {
return [file dirname [punk::winpath $path]]
}
#-------------------------------------------------------------------
#sh 'test' equivalent - to be used with exitcode of process
#
#single evaluation to get exitcode
proc sh_test {args} {
tailcall run test {*}$args
}
#double-evaluation to get true/fals
#faster tcl equivalents where possible to accuratley provide, and fallthrough to sh for compatibility of unimplemented
#The problem with fallthrough is that sh/bash etc have a different view of existant files
#e.g unix files such as /dev/null vs windows devices such as CON,PRN
#e.g COM1 is mapped as /dev/ttyS1 in wsl (?)
proc sh_TEST {args} {
set a1 [lindex $args 0]
set a2 [lindex $args 1]
set a3 [lindex $args 2]
if {[llength $args] == 1} {
#equivalent of -n STRING
return [expr {[string length $a1] != 0}]
} elseif {[llength $args] == 2} {
switch -- $a1 {
-b {
#dubious utility on FreeBSD, windows?
#FreeBSD has dropped support for block devices - stating 'No serious applications rely on block devices'
#Linux apparently uses them though
if{[file exists $a2]} {
if {[file type $a2] eq "blockSpecial"} {
return true
} else {
return false
}
} else {
return false
}
}
-c {
#e.g on windows CON,NUL
if {[file exists $a2]} {
if {[file type $a2] eq "characterSpecial"} {
return true
} else {
return false
}
} else {
return false
}
}
-d {
return [file isdirectory $a2]
}
-e {
return [file exists $a2]
}
-f {
#e.g on windows CON,NUL
if {[file exists $a2]} {
if {[file type $a2] eq "file"} {
return true
} else {
return false
}
} else {
return false
}
}
-h -
-L {
return [expr {[file type $a2] eq "link"}]
}
-s {
if {[file exists $a2] && ([file size $a2] > 0 )} {
return true
} else {
return false
}
}
-S {
if {[file exists $a2]} {
if {[file type $a2] eq "socket"} {
return true
} else {
return false
}
} else {
return false
}
}
-x {
if {[file exists $a2] && [file executable $a2]} {
return true
} else {
return false
}
}
-w {
if {[file exists $a2] && [file writable $a2]} {
return true
} else {
return false
}
}
-z {
return [expr {[string length $a2] == 0}]
}
-n {
return [expr {[string length $a2] != 0}]
}
default {
tailcall apply {arglist {uplevel #0 [run test {*}$arglist]} ::} $args
}
}
} elseif {[llength $args] == 3} {
switch -- $a2 {
"=" {
return [string equal $a1 $a3]
}
"!=" {
return [expr {$a1 ne $a3}]
}
"-eq" {
if {![string is integer -strict $a1]} {
puts stderr "sh_TEST: invalid integer '$a1'"
return false
}
if {![string is integer -strict $a3]} {
puts stderr "sh_TEST: invalid integer '$a3'"
return false
}
return [expr {$a1 == $a3}]
}
"-ge" {
return [expr {$a1 >= $a3}]
}
"-gt" {
return [expr {$a1 > $a3}]
}
"-le" {
return [expr {$a1 <= $a3}]
}
"-lt" {
return [expr {$a1 < $a3}]
}
"-ne" {
return [expr {$a1 != $a3}]
}
default {
tailcall apply {arglist {uplevel #0 [run test {*}$arglist]} ::} $args
}
}
} else {
tailcall apply {arglist {uplevel #0 [run test {*}$arglist]} ::} $args
}
}
proc sh_echo {args} {
tailcall run echo {*}$args
}
proc sh_ECHO {args} {
tailcall apply {arglist {uplevel #0 [run echo {*}$arglist]} ::} $args
}
#sh style true/false for process exitcode. 0 is true - everything else false
proc exitcode {args} {
set c [lindex $args 0]
if {[string is integer -strict $c]} {
#return [expr {$c == 0}]
#return true/false to make it clearer we are outputting tcl-boolean inverse mapping from the shell style 0=true
if {$c == 0} {
return true
} else {
return false
}
} else {
return false
}
}
#-------------------------------------------------------------------
namespace export help aliases alias cdwin cdwindir winpath windir app
namespace ensemble create
#todo - in thread
#todo - streaming version
proc dirfiles_lists {{glob ""}} {
set dir [pwd]
if {$glob eq ""} {
set glob "*"
}
set dirname [file dirname $glob] ;# for * or something* will return just "." which is ok
set ftail [file tail $glob]
if {[string first ? $glob] >= 0 || [string first * $glob] >=0} {
#has globchar (we only recognise in tail)
set location $dirname
set glob $ftail
} else {
set location $dirname/$ftail
set glob *
}
set dirs [glob -nocomplain -directory $location -type d -tail $glob]
set files [glob -nocomplain -directory $location -type f -tail $glob]
return [list dirs $dirs files $files]
}
proc dirfiles {{glob ""}} {
package require overtype
set contents [dirfiles_lists $glob]
set dirs [dict get $contents dirs]
set files [dict get $contents files]
set widest 4
foreach d $dirs {
set w [string length $d]
if {$w > $widest} {
set widest $w
}
}
set displaylist [list]
set col1 [string repeat " " [expr {$widest + 2}]]
foreach d $dirs f $files {
lappend displaylist [overtype::left $col1 $d]$f
}
return [list_as_lines $displaylist]
}
#tailcall is important
#TODO - fix. conflicts with Tk toplevel command "."
proc ./ {args} {
set ::punk::last_run_display [list]
if {([llength $args]) && ([lindex $args 0] eq "")} {
set args [lrange $args 1 end]
}
if {![llength $args]} {
#ls is too slow even over a fairly low-latency network
#set out [runout -n ls -aFC]
set out [punk::dirfiles]
#puts stdout $out
#puts stderr [a+ white]$out[a+]
set result [pwd]
set chunklist [list]
lappend chunklist [list stdout "[a+ white light]$out[a+]\n"]
lappend chunklist [list result $result]
set ::punk::last_run_display $chunklist
if {$::repl::running} {
repl::term::set_console_title [file normalize $result]
}
return $result
} else {
#set a1 [lindex $args 0]
set atail [lassign $args a1]
if {$a1 in [list . .. "./" "../"]} {
if {$a1 in [list ".." "../"]} {
cd $a1
}
tailcall punk::./ {*}$atail
}
set curdir [pwd]
set ptype [file pathtype $a1]
if {$ptype eq "absolute"} {
set path $a1
} elseif {$ptype eq "volumerelative"} {
if {$::tcl_platform(platform) eq "windows"} {
#unix looking paths like /c/users or /usr/local/etc are reported by tcl as volumerelative.. (as opposed to absolute on unix platforms)
if {[string index $a1 0] eq "/"} {
set path [punk::winpath $a1]
#puts stderr "winpath: $path"
} else {
set path $curdir/$a1
}
} else {
# unknown what paths are reported as this on other platforms.. treat as absolute for now
set path $a1
}
} else {
set path $curdir/$a1
}
if {[file type $path] eq "file"} {
if {[string tolower [file extension $path]] in [list ".tcl" ".tm"]} {
set newargs $atail
set ::argv0 $path
set ::argc [llength $newargs]
set ::argv $newargs
tailcall source $path
} else {
puts stderr "Cannot run [file extension $path] file directly ([file tail $path])"
return [pwd]
}
}
if {[file type $path] eq "directory"} {
cd $path
tailcall punk::./ {*}$atail
}
error "Cannot access path $path"
}
}
proc ../ {args} {
set ::punk::last_run_display [list]
if {![llength $args]} {
set path ..
} else {
set path ../[file join {*}$args]
}
cd $path
#set out [runout -n ls -aFC]
set out [punk::dirfiles]
set result [pwd]
#return $out\n[pwd]
set chunklist [list]
lappend chunklist [list stdout "[a+ white light]$out[a+]\n"]
lappend chunklist [list result $result]
set ::punk::last_run_display $chunklist
if {$::repl::running} {
repl::term::set_console_title $result
}
return $result
}
proc list_as_lines {list {joinchar \n}} {
join $list $joinchar
}
proc ls {args} {
if {![llength $args]} {
set args [list [pwd]]
}
if {[llength $args] ==1} {
return [glob -nocomplain -tails -dir [lindex $args 0] *]
} else {
set result [dict create]
foreach a $args {
set k [file normalize $a]
set contents [glob -nocomplain -tails -dir $a *]
dict set result $k $contents
}
return $result
}
}
proc cdwin {path} {
set path [punk::winpath $path]
if {$::repl::running} {
repl::term::set_console_title $path
}
cd $path
}
proc cdwindir {path} {
set path [punk::winpath $path]
if {$::repl::running} {
repl::term::set_console_title $path
}
cd [file dirname $path]
}
#like linelist - but keeps leading and trailing empty lines
#single \n produces {} {}
#the result can be joined to reform the arg if a single arg supplied
#
proc linelistraw {args} {
set linelist [list]
foreach {a} $args {
set nsplit [split $a \n]
lappend linelist {*}$nsplit
}
#return [split $text \n]
return $linelist
}
proc linelist1 {args} {
set linelist [list]
foreach {a} $args {
set nsplit [split $a \n]
set start 0
set end "end"
if {[lindex $nsplit 0] eq ""} {
set start 1
}
if {[lindex $nsplit end] eq ""} {
set end "end-1"
}
set alist [lrange $nsplit $start $end]
lappend linelist {*}$alist
}
return $linelist
}
# important for match_exec & match_assign
# lineval verbatim|trimmed
proc linelist {text {lineval verbatim}} {
if {$lineval ni [list verbatim trimmed]} {error "linelist 2nd argument valid values are 'verbatim' or 'trimmed'"}
set linelist [list]
if {[string first \n $text] < 0} {
return $text
}
set nsplit [split $text \n]
set start 0
set end "end"
if {[lindex $nsplit 0] eq ""} {
set start 1
}
if {[lindex $nsplit end] eq ""} {
set end "end-1"
}
set alist [lrange $nsplit $start $end]
if {$lineval eq "verbatim"} {
set linelist $alist
#lappend linelist {*}$alist
} else {
foreach ln $alist {
lappend linelist [string trim $ln]
}
}
return $linelist
}
#!!!todo fix - linedict is unfinished and non-functioning
#linedict based on indents
proc linedict {args} {
set data [lindex $args 0]
set opts [lrange $args 1 end] ;#todo
set nsplit [split $data \n]
set rootindent -1
set stepindent -1
#set wordlike_parts [regexp -inline -all {\S+} $lastitem]
set d [dict create]
set keys [list]
set i 1
set firstkeyline "N/A"
set firststepline "N/A"
foreach ln $nsplit {
if {![string length [string trim $ln]]} {
incr i
continue
}
set is_rootkey 0
regexp {(\s*)(.*)} $ln _ space linedata
puts stderr ">>line:'$ln' [string length $space] $linedata"
set this_indent [string length $space]
if {$rootindent < 0} {
set firstkeyline $ln
set rootindent $this_indent
}
if {$this_indent == $rootindent} {
set is_rootkey 1
}
if {$this_indent < $rootindent} {
error "bad root indentation ($this_indent) at line: $i smallest indent was set by first key line: $firstkeyline"
}
if {$is_rootkey} {
dict set d $linedata {}
lappend keys $linedata
} else {
if {$stepindent < 0} {
set stepindent $this_indent
set firststepline $ln
}
if {$this_indent == $stepindent} {
dict set d [lindex $keys end] $ln
} else {
if {($this_indent % $stepindent) != 0} {
error "bad indentation ($this_indent) at line: $i not a multiple of the first key indent $step_indent seen on $firststepline"
}
#todo fix!
set parentkey [lindex $keys end]
lappend keys [list $parentkey $ln]
set oldval [dict get $d $parentkey]
if {[string length $oldval]} {
set new [dict create $oldval $ln]
} else {
dict set d $parentkey $ln
}
}
}
incr i
}
return $d
}
proc dictline {d} {
puts stderr "unimplemented"
set lines [list]
return $lines
}
#return list of {chan chunk} elements
proc help_chunks {} {
set chunks [list]
set linesep [string repeat - 76]
catch {
package require patternpunk
#puts -nonewline stderr [>punk . rhs]
lappend chunks [list stderr [>punk . rhs]]
}
set text ""
set known $::punk::config::known_punk_env_vars
append text $linesep\n
append text "punk environment vars:\n"
append text $linesep\n
set col1 [string repeat " " 25]
set col2 [string repeat " " 50]
foreach v $known {
set c1 [overtype::left $col1 $v]
if {[info exists ::env($v)]} {
set c2 [overtype::left $col2 [set ::env($v)]
} else {
set c2 [overtype::right $col2 "(NOT SET)"]
}
append text "$c1 $c2\n"
}
append text $linesep\n
lappend chunks [list stdout $text]
set text ""
append text "Punk commands:\n"
append text "punk help\n"
lappend chunks [list stdout $text]
return $chunks
}
proc help {} {
set chunks [help_chunks]
foreach chunk $chunks {
lassign $chunk chan text
puts -nonewline $chan $text
}
}
proc app {{glob *}} {
upvar ::punk::config::running running_config
set apps_folder [dict get $running_config apps]
if {[file exists $apps_folder]} {
if {[file exists $apps_folder/$glob]} {
tailcall source $apps_folder/$glob/main.tcl
}
set apps [glob -nocomplain -dir $apps_folder -type d -tail $glob]
if {[llength $apps] == 0} {
if {[string first * $glob] <0 && [string first ? $glob] <0} {
#no glob chars supplied - only launch if exact match for name part
set namematches [glob -nocomplain -dir $apps_folder -type d -tail ${glob}-*]
set namematches [lsort $namematches] ;#todo - -ascii? -dictionary? natsort?
if {[llength $namematches] > 0} {
set latest [lindex $namematches end]
lassign $latest nm ver
tailcall source $apps_folder/$latest/main.tcl
}
}
}
return $apps
}
}
#current interp aliases except those created by pattern package '::p::*'
proc aliases {{glob *}} {
#todo - way to configure and query what aliases are hidden
set interesting [lmap a [interp aliases ""] {expr {![string match ::* $a] ? $a : [continue]}}]
#set interesting [lmap a [interp aliases ""] {expr {![string match ::p::* $a] ? $a : [continue]}}]
set interesting [lmap a $interesting {expr {![string match *twapi::* $a] ? $a : [continue]}}]
set interesting [lmap a $interesting {expr {![string match debug.* $a] ? $a : [continue]}}]
#set interesting [lmap a $interesting {expr {![string match *vfs::* $a] ? $a : [continue]}}]
set matched [lsearch -all -inline $interesting $glob]
}
proc alias {{aliasorglob ""} args} {
if {[llength $args]} {
if {$aliasorglob in [interp aliases ""]} {
set existing [interp alias "" $aliasorglob]
puts stderr "Overwriting existing alias $aliasorglob -> $existing with $aliasorglob -> $args (in current session only)"
}
if {([llength $args] ==1) && [string trim [lindex $args 0]] eq ""} {
#use empty string/whitespace as intention to delete alias
return [interp alias "" $aliasorglob ""]
}
return [interp alias "" $aliasorglob "" {*}$args]
} else {
if {![string length $aliasorglob]} {
set aliaslist [punk aliases]
puts -nonewline stderr $aliaslist
return
}
if {([string first "*" $aliasorglob] >= 0) || ([string first "?" $aliasorglob] >= 0)} {
set aliaslist [punk aliases $aliasorglob]
puts -nonewline stderr $aliaslist
return
}
return [interp alias "" $aliasorglob]
}
}
#know is critical to the punk repl for proper display output
interp alias {} know {} punk::know
interp alias {} know? {} punk::know?
#interp alias {} arg {} punk::val
interp alias {} val {} punk::val
interp alias {} exitcode {} punk::exitcode
interp alias {} hide {} punkapp::hide_console ;#will only work if controllable toplevels exist
interp alias {} ansi {} punk::ansi
interp alias {} a+ {} punk::ansi+
#sh style 'test' and 'exitcode' (0 is false)
interp alias {} sh_test {} punk::sh_test
interp alias {} sh_echo {} punk::sh_echo
interp alias {} sh_TEST {} punk::sh_TEST
interp alias {} sh_ECHO {} punk::sh_ECHO
#friendly sh aliases (which user may wish to disable e.g if conflicts)
interp alias {} test {} punk::sh_test ;#not much reason to run 'test' directly in punk shell (or tclsh shell) as returncode not obvious anyway due to use of exec
interp alias {} TEST {} punk::sh_TEST; #double-evaluation to return tcl true/false from exitcode
interp alias {} echo {} punk::sh_echo
interp alias {} ECHO {} punk::sh_ECHO
#interp alias {} c {} clear ;#external executable 'clear' may not always be available
interp alias {} clear {} repl::term::reset
interp alias {} c {} repl::term::reset
interp alias {} help {} punk help
interp alias {} aliases {} punk aliases
interp alias {} alias {} punk alias
interp alias {} treemore {} punk::xmore tree
#----------------------------------------------
interp alias {} linelistraw {} punk::linelistraw
interp alias {} linelist {} punk::linelist ;#critical for = assignment features
interp alias {} linedict {} punk::linedict
interp alias {} dictline {} punk::dictline
interp alias {} % {} punk::%
interp alias {} pipeswitch {} punk::pipeswitch
interp alias {} pipecase {} punk::pipecase
interp alias {} pipematch {} punk::pipematch
interp alias {} ispipematch {} punk::ispipematch
interp alias {} pipenomatchvar {} punk::pipenomatchvar
interp alias {} nscommands {} ,'ok@0.= {
upvar caseresult caseresult
if {![info exists ns]} {
set ns ""
}
pipeswitch {
#no glob chars present
pipecase \
caseresult.= val $ns |input> \
1.= {expr {[string length [string map [list * "" ? ""] $data]] == [string length $data]}} |> {
uplevel #0 [list info commands ${input}::*]
}
#pipecase1 ns has one or more of glob chars * or ?
pipecase \
caseresult.= val $ns |input> {
uplevel #0 [list info commands ${input}]
}
}
} |data@@ok/result> {set data} |> {lmap v $data {namespace tail $v}} |> lsort |> {join $data \n}