package provide punk [namespace eval punk { variable version set version 0.1 }] namespace eval punk::config { variable loaded variable startup ;#include env overrides variable running set vars [list \ scriptlib \ color_stdout \ color_stderr \ logfile_stdout \ logfile_stderr \ syslog_stdout \ syslog_stderr \ exec_unknown \ ] #todo pkg punk::config #defaults dict set startup exec_unknown true ;#whether to use exec instead of experimental shellfilter::run dict set startup color_stdout [list cyan bold] dict set startup color_stderr [list red bold] dict set startup syslog_stdout "127.0.0.1:514" dict set startup syslog_stderr "127.0.0.1:514" #default file logs to logs folder at same location as exe if writable, or empty string dict set startup logfile_stdout "" dict set startup logfile_stderr "" set exefolder [file dirname [info nameofexecutable]] set log_folder $exefolder/logs dict set startup scriptlib $exefolder/scriptlib if {[file exists $log_folder]} { if {[file isdirectory $log_folder] && [file writable $log_folder]} { dict set startup logfile_stdout $log_folder/repl-exec-stdout.txt dict set startup logfile_stderr $log_folder/repl-exec-stderr.txt } } #todo - load/write config file #env vars override the configuration #todo - define which configvars are settable in env set known_punk_env_vars [list \ PUNK_SCRIPTLIB \ PUNK_EXECUNKNOWN \ PUNK_COLOR_STDERR \ PUNK_COLOR_STDOUT \ PUNK_LOGFILE_STDOUT \ PUNK_LOGFILE_STDERR \ PUNK_SYSLOG_STDOUT \ PUNK_SYSLOG_STDERR \ ] #override with env vars if set foreach evar $known_punk_env_vars { if {[info exists ::env($evar)]} { set f [set ::env($evar)] if {$f ne "default"} { #e.g PUNK_SCRIPTLIB -> scriptlib set varname [string tolower [string range $evar 5 end]] dict set startup $varname $f } } } set running [dict create] set running [dict merge $running $startup] } namespace eval punk { 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} { {*}$args | more } proc winpath {path} { #convert /c/etc to C:/etc set re {^/([[:alpha:]]){1}/.*} 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 $path _ letter]} { #upper case appears to be windows canonical form if {[string toupper $letter] in $driveletters} { set path [string toupper $letter]:/[string range $path 3 end] } } elseif {[regexp {^/mnt|MNT/([[:alpha:]]){1}/.*} $path _ letter]} { if {[string toupper $letter] in $driveletters} { set path [string toupper $letter]:/[string range $path 7 end] } } #puts stderr "=> $path" #things like 'which' seem to return a path minus the .exe - so we'll just test the containing folder if {![file exists [file dirname $path]]} { set path [file normalize $path] } 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 namespace ensemble create #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]} { set out [runout -n ls -aFC] #puts stdout $out #puts stderr [a+ white]$out[a+] set result [pwd] set chunklist [list] lappend chunklist [list stderr "[a+ white light]$out[a+]\n"] lappend chunklist [list stdout $result\n] set ::punk::last_run_display $chunklist return $result } else { set a1 [lindex $args 0] if {$a1 in [list . .. "./" "../"]} { if {$a1 in [list ".." "../"]} { cd $a1 } tailcall punk::./ {*}[lrange $args 1 end] } set curdir [pwd] set path $curdir/$a1 if {[file type $path] eq "file"} { if {[string tolower [file extension $path]] in [list ".tcl" ".tm"]} { set newargs [lrange $args 1 end] 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::./ {*}[lrange $args 1 end] } error "Cannot access path $path" } } proc ../ {args} { set ::punk::last_run_display [list] if {![llength $args]} { cd .. } else { cd ../[file join {*}$args] } set out [runout -n ls -aFC] set result [pwd] #return $out\n[pwd] set chunklist [list] lappend chunklist [list stderr "[a+ white light]$out[a+]\n"] lappend chunklist [list stdout $result\n] set ::punk::last_run_display $chunklist return $result } 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] cd $path } proc cdwindir {path} { set path [punk::winpath $path] cd [file dirname $path] } #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 } } #current interp aliases except those created by pattern package '::p::*' proc aliases {{glob *}} { set interesting [lmap a [interp aliases ""] {expr {![string match ::p::* $a] ? $a : [continue]}}] set matched [lsearch -all -inline $interesting $glob] } proc alias {a args} { if {[llength $args]} { if {$a in [interp aliases ""]} { set existing [interp alias "" $a] puts stderr "Overwriting existing alias $a -> $existing with $a -> $args (in current session only)" } interp alias "" $a "" {*}$args } else { return [interp alias "" $a] } } #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 interp alias {} exitcode {} punk::exitcode #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 interp alias {} help {} punk help interp alias {} aliases {} punk aliases interp alias {} alias {} punk alias interp alias {} treemore {} punk::xmore tree #---------------------------------------------- #leave the winpath related aliases available on all platforms interp alias {} cdwin {} punk cdwin interp alias {} cdwindir {} punk cdwindir interp alias {} winpath {} punk winpath interp alias {} windir {} punk windir #---------------------------------------------- #git interp alias {} gs {} git status -sb interp alias {} gl {} git log --oneline --decorate ;#decorate so stdout consistent with what we see on console interp alias {} glast {} git log -1 HEAD --stat interp alias {} gconf {} git config --global -l #---------------------------------------------- # ls aliases - note that tcl doesn't exand * but sh_xxx functions pass to sh -c allowing shell expansion interp alias {} l {} sh_runout -n ls -A ;#plain text listing #interp alias {} ls {} sh_runout -n ls -AF --color=always interp alias {} ls {} unknown ls -AF --color=always ;#use unknown to use terminal and allow | more | less #note that shell globbing with * won't work on unix systems when using unknown/exec interp alias {} lw {} sh_runout -n ls -AFC --color=always ;#wide listing (use A becaus no extra info on . & ..) interp alias {} ll {} sh_runout -n ls -laFo --color=always ;#use a instead of A to see perms/owner of . & .. # -v for natural number sorting not supported on freeBSD. Todo - test at startup and modify aliases? #interp alias {} lw {} ls -aFv --color=always interp alias {} ./ {} punk::./ interp alias {} ../ {} punk::../ if {$::tcl_platform(platform) eq "windows"} { set has_powershell 1 interp alias {} dl {} dir /q interp alias {} dw {} dir /W/D } else { #todo - natsorted equivalent #interp alias {} dl {} #todo - powershell detection on other platforms set has_powershell 0 } if {$has_powershell} { interp alias {} psls {} pwsh -nop -nolo -c ls interp alias {} psps {} pwsh -nop -nolo -c ps } }