diff --git a/src/modules/punk/mix/templates/utility/scriptappwrappers/basic/multishell-old.cmd b/src/modules/punk/mix/templates/utility/scriptappwrappers/basic/multishell-old.cmd
deleted file mode 100644
index 1cb9e0ef..00000000
--- a/src/modules/punk/mix/templates/utility/scriptappwrappers/basic/multishell-old.cmd
+++ /dev/null
@@ -1,270 +0,0 @@
-set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershell;proc Hide s {proc $s args {}}; Hide :;rename set s2;Hide set;s2 1 list]"; set -- : "$@"; $1 = @'
-: heredoc1 - hide from powershell (close sqote for unix shells) ' \
-: << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH'
-: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl \
-: "[Hide @ECHO; Hide ); Hide (;Hide echo]#not necessary but can help avoid errs in testing"
-: Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \
-@REM {
-@REM DO NOT MODIFY FIRST LINE OF THIS SCRIPT. shebang #! line is not required and will reduce functionality.
-@REM Even comment lines can be part of the functionality of this script - modify with care.
-@REM Change the value of nextshell in the next line if desired, and code within payload sections as appropriate.
-@SET "nextshell=pwsh"
-@REM nextshell set to pwsh,sh,bash or tclsh
-@REM @ECHO nextshell is %nextshell%
-@SET "validshells=pwsh,sh,bash,tclsh"
-@CALL SET keyRemoved=%%validshells:%nextshell%=%%
-@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available
-@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-@REM -- cmd/batch file section (ignored on unix)
-@REM -- This section intended only to launch the next shell
-@REM -- Avoid customising this if possible. cmd/batch script is probably the least expressive language.
-@REM -- custom windows payloads should be in powershell,tclsh or sh/bash code sections
-@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-@SETLOCAL EnableExtensions EnableDelayedExpansion
-@SET "winpath=%~dp0"
-@SET "fname=%~nx0"
-@REM @ECHO fname %fname%
-@REM @ECHO winpath %winpath%
-@IF %nextshell%==pwsh (
- CALL pwsh -nop -c set-executionpolicy -Scope CurrentUser RemoteSigned
- COPY "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >NUL
- REM test availability of preferred option of powershell7+ pwsh
- CALL pwsh -nop -nol -c write-host "statusmessage: pwsh-found" >NUL
- SET pwshtest_exitcode=!errorlevel!
- REM ECHO pwshtest_exitcode !pwshtest_exitcode!
- IF !pwshtest_exitcode!==0 CALL pwsh -nop -nol "%~dp0%~n0.ps1" %* & SET task_exitcode=!errorlevel!
- REM fallback to powershell if pwsh failed
- IF NOT !pwshtest_exitcode!==0 (
- REM CALL powershell -nop -nol -c write-host powershell-found
- CALL powershell -nop -nol -file "%~dp0%~n0.ps1" %*
- SET task_exitcode=!errorlevel!
- )
-) ELSE (
- IF %nextshell%==bash (
- CALL :getWslPath %winpath% wslpath
- REM ECHO wslfullpath "!wslpath!%fname%"
- CALL %nextshell% "!wslpath!%fname%" %* & SET task_exitcode=!errorlevel!
- ) ELSE (
- REM probably tclsh or sh
- IF NOT "x%keyRemoved%"=="x%validshells%" (
- REM sh uses /c/ instead of /mnt/c - at least if using msys. Todo, review what is the norm on windows with and without msys2,cygwin,wsl
- REM and what logic if any may be needed. For now sh with /c/xxx seems to work the same as sh with c:/xxx
- CALL %nextshell% "%~dp0%fname%" %* & SET task_exitcode=!errorlevel!
- ) ELSE (
- ECHO %fname% has invalid nextshell value %nextshell% valid options are %validshells%
- SET task_exitcode=66
- GOTO :exit
- )
- )
-)
-@GOTO :endlib
-:getWslPath
-@SETLOCAL
- @SET "_path=%~p1"
- @SET "name=%~nx1"
- @SET "drive=%~d1"
- @SET "rtrn=%~2"
- @SET "result=/mnt/%drive:~0,1%%_path:\=/%%name%"
-@ENDLOCAL & (
- @if "%~2" neq "" (
- SET "%rtrn%=%result%"
- ) ELSE (
- ECHO %result%
- )
-)
-@GOTO :eof
-:endlib
-
-: \
-@REM @SET taskexit_code=!errorlevel! & goto :exit
-@GOTO :exit
-# }
-# rem call %nextshell% "%~dp0%~n0.cmd" %*
-# -*- tcl -*-
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- tcl script section
-# -- This is a punk multishell file
-# -- Primary payload target is Tcl, with sh,bash,powershell as helpers
-# -- but it may equally be used with any of these being the primary script.
-# -- It is tuned to run when called as a batch file, a tcl script a sh/bash script or a pwsh/powershell script
-# -- i.e it is a polyglot file.
-# -- The specific layout including some lines that appear just as comments is quite sensitive to change.
-# -- It can be called on unix or windows platforms with or without the interpreter being specified on the commandline.
-# -- e.g ./filename.polypunk.cmd in sh or bash
-# -- e.g tclsh filename.cmd
-# --
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-rename set ""; rename s2 set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup
-Hide :exit;Hide {<#};Hide '@
-namespace eval ::punk::multishell {
- set last_script_root [file dirname [file normalize ${argv0}/__]]
- set last_script [file dirname [file normalize [info script]/__]]
- if {[info exists argv0] &&
- $last_script eq $last_script_root
- } {
- set ::punk::multishell::is_main($last_script) 1 ;#run as executable/script - likely desirable to launch application and return an exitcode
- } else {
- set ::punk::multishell::is_main($last_script) 0 ;#sourced - likely to be being used as a library - no launch, no exit. Can use return.
- }
- if {"::punk::multishell::is_main" ni [info commands ::punk::multishell::is_main]} {
- proc ::punk::multishell::is_main {{script_name {}}} {
- if {$script_name eq ""} {
- set script_name [file dirname [file normalize [info script]/--]]
- }
- if {![info exists ::punk::multishell::is_main($script_name)]} {
- #e.g a .dll or something else unanticipated
- puts stderr "Warning punk::multishell didn't recognize info script result: $script_name - will treat as if sourced and return instead of exiting"
- puts stderr "Info: script_root: [file dirname [file normalize ${argv0}/__]]"
- return 0
- }
- return [set ::punk::multishell::is_main($script_name)]
- }
- }
-}
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload
-#puts "script : [info script]"
-#puts "argcount : $::argc"
-#puts "argvalues: $::argv"
-#puts "argv0 : $::argv0"
-# -- --- --- --- --- --- --- --- --- --- --- ---
-
-
-#
-#
-
-
-
-# -- --- --- --- --- --- --- --- --- --- --- ---
-# -- Best practice is to always return or exit above, or just by leaving the below defaults in place.
-# -- If the multishell script is modified to have Tcl below the Tcl Payload section,
-# -- then Tcl bracket balancing needs to be carefully managed in the shell and powershell sections below.
-# -- Only the # in front of the two relevant if statements below needs to be removed to enable Tcl below
-# -- but the sh/bash 'then' and 'fi' would also need to be uncommented.
-# -- This facility left in place for experiments on whether configuration payloads etc can be appended
-# -- to tail of file - possibly binary with ctrl-z char - but utility is dependent on which other interpreters/shells
-# -- can be made to ignore/cope with such data.
-if {[::punk::multishell::is_main]} {
- exit 0
-} else {
- return
-}
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload
-# end hide from unix shells \
-HEREDOC1B_HIDE_FROM_BASH_AND_SH
-# sh/bash \
-shift && set -- "${@:1:$#-1}"
-#------------------------------------------------------
-# -- This if block only needed if Tcl didn't exit or return above.
-if false==false # else {
- then
- :
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- sh/bash script section
-# -- leave as is if all that is required is launching the Tcl payload"
-# --
-# -- Note that sh/bash script isn't called when running a .bat/.cmd from cmd.exe on windows by default
-# -- adjust @call line above ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate
-# -- if sh/bash scripting needs to run on windows too.
-# --
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload
-#printf "start of bash or sh code"
-
-#
-#
-
-# -- --- --- --- --- --- --- ---
-#
-exitcode=0 ;#default assumption
-#-- sh/bash launches Tcl here instead of shebang line at top
-#-- use exec to use exitcode (if any) directly from the tcl script
-#exec /usr/bin/env tclsh "$0" "$@"
-#-- alternative - can run sh/bash script after the tcl call.
-/usr/bin/env tclsh "$0" "$@"
-exitcode=$?
-#echo "tcl exitcode: ${exitcode}"
-#-- override exitcode example
-#exit 66
-#
-# -- --- --- --- --- --- --- ---
-
-#
-#
-
-
-#printf "sh/bash done \n"
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end sh Payload
-#------------------------------------------------------
-fi
-exit ${exitcode}
-# end hide sh/bash block from Tcl
-# This comment with closing brace should stay in place whether if commented or not }
-#------------------------------------------------------
-# begin hide powershell-block from Tcl - only needed if Tcl didn't exit or return above
-if 0 {
-: end heredoc1 - end hide from powershell \
-'@
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- powershell/pwsh section
-# --
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-function GetScriptName { $myInvocation.ScriptName }
-$scriptname = getScriptName
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload
-#"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host
-#"Script Name : {0}" -f $scriptname | write-host
-#"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host
-#"powershell args : {0}" -f ($args -join ", ") | write-host
-# -- --- --- ---
-
-#
-#
-
-
-# -- --- --- --- --- --- --- ---
-#
-tclsh $scriptname $args
-#
-# -- --- --- --- --- --- --- ---
-
-
-#
-#
-
-# unbal }
-
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload
-#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host
-Exit $LASTEXITCODE
-# heredoc2 for powershell to ignore block below
-$1 = @'
-'
-: end hide powershell-block from Tcl \
-# This comment with closing brace should stay in place whether 'if' commented or not }
-: cmd exit label - return exitcode
-:exit
-: \
-@REM @ECHO exitcode: !task_exitcode!
-: \
-@EXIT /B !task_exitcode!
-# cmd has exited
-: end heredoc2 \
-'@
-<#
-# id:tailblock0
-# -- powershell multiline comment
-#>
-<#
-# id:tailblock1
-#
-
-#
-# -- unreachable by tcl directly if ctrl-z character is in the section above. (but file can be read and split on \x1A)
-# -- Potential for zip and/or base64 contents, but we can't stop pwsh parser from slurping in the data
-# -- so for example a plain text tar archive could cause problems depending on the content.
-# -- final line in file must be the powershell multiline comment terminator or other data it can handle.
-# -- e.g plain # comment lines will work too
-# -- (for example a powershell digital signature is a # commented block of data at the end of the file)
-#>
-
-
diff --git a/src/modules/punk/mix/templates/utility/scriptappwrappers/basic/shellbat.bat b/src/modules/punk/mix/templates/utility/scriptappwrappers/basic/shellbat.bat
deleted file mode 100644
index aa9039a9..00000000
--- a/src/modules/punk/mix/templates/utility/scriptappwrappers/basic/shellbat.bat
+++ /dev/null
@@ -1,112 +0,0 @@
-: "[proc : args {}]" ;# *tcl shellbat - call with sh,bash,tclsh on any platform, or with cmd on windows.
-: <<'HIDE_FROM_BASH_AND_SH'
-: ;# leading colon hides from .bat, trailing slash hides next line from tcl \
-@call tclsh "%~dp0%~n0.bat" %*
-: ;#\
-@set taskexitcode=%errorlevel% & goto :exit
-# -*- tcl -*-
-# #################################################################################################
-# This is a tcl shellbat file
-# It is tuned to run when called as a batch file, a tcl script, an sh script or a bash script,
-# so the specific layout and characters used are quite sensitive to change.
-# It can be called on unix or windows platforms with or without the interpreter being specified on the commandline.
-# e.g ./filename.sh.bat in sh or bash or powershell
-# e.g filename.sh or filename.sh.bat at windows command prompt
-# e.g tclsh filename.sh.bat | sh filename.sh.bat | bash filename.sh.bat
-# In all cases an arbitrary number of arguments are accepted
-# To avoid the initial commandline on stdout when calling as a batch file on windows, use:
-# cmd /Q /c filename.sh.bat
-# (because we cannot use @if to silence it, as this isn't understood by tcl,sh or bash)
-# #################################################################################################
-#fconfigure stdout -translation crlf
-# --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload
-#puts "script : [info script]"
-#puts "argcount : $::argc"
-#puts "argvalues: $::argv"
-
-
-#
-#
-
-# --- --- --- --- --- --- --- --- --- --- --- --- ---
-# only exit if needed. see exitcode notes at bottom of file and exit there for consistency across invocation methods
-# --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload
-#--
-#-- bash/sh code follows.
-#-- protect from tcl using line continuation char on the previous comment for each line, like so: \
-printf "etc"
-#-- or alternatively place sh/bash script within the false==false block
-#-- whilst being careful to balance braces {}
-#-- For more complex needs you should call out to external scripts
-#--
-#-- END marker for hide_from_bash_and_sh\
-HIDE_FROM_BASH_AND_SH
-
-#---------------------------------------------------------
-#-- This if statement hides(mostly) a sh/bash code block from Tcl
-if false==false # else {
-then
-:
-#---------------------------------------------------------
- #-- leave as is if all that's required is launching the Tcl payload"
- #--
- #-- Note that sh/bash script isn't called when running a .bat from cmd.exe on windows by default
- #-- adjust line 4: @call tclsh ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate
- #-- if sh/bash scripting needs to run on windows too.
- #--
- #printf "start of bash or sh code"
-
- #
- #
-
-
- #-- sh/bash launches Tcl here instead of shebang line at top
- #
- #-- use exec to use exitcode (if any) directly from the tcl script
- exec /usr/bin/env tclsh "$0" "$@"
- #
-
- #-- alternative - if sh/bash script required to run after the tcl call.
- #/usr/bin/env tclsh "$0" "$@"
- #tcl_exitcode=$?
- #echo "tcl_exitcode: ${tcl_exitcode}"
-
- #
- #
-
- #-- override exitcode example
- #exit 66
-
- #printf "No need for trailing slashes for sh/bash code here\n"
-#---------------------------------------------------------
-fi
-# closing brace for Tcl }
-#---------------------------------------------------------
-
-#-- tcl and shell script now both active
-
-#-- comment for line sample 1 with trailing continuation slash \
-#printf "tcl-invisible sh/bash line sample 1 \n"
-
-#-- comment for line sample 2 with trailing continuation slash \
-#printf "tcl-invisible sh/bash line sample 2 \n"
-
-
-#-- Consistent exitcode from sh,bash,tclsh or cmd
-#-- Call exit in tcl (or sh/bash) code only if explicitly required, otherwise leave this commented out.
-#-- (script might be more widely useable without explicit exit. e.g in tcl: set ::argc 1; set ::argv "val"; source filename.sh.bat )
-#-- exit line unprotected by trailing slash will work for tcl and/or sh/bash
-#exit 0
-#exit 42
-
-
-
-#-- make sure sh/bash/tcl all skip over .bat style exit \
-: <<'shell_end'
-#-- .bat exit with exitcode from tcl process \
-:exit
-: ;# \
-@exit /B %taskexitcode%
-# .bat has exited \
-shell_end
-
diff --git a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell2.cmd b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell2.cmd
deleted file mode 100644
index 9daf7ebf..00000000
--- a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell2.cmd
+++ /dev/null
@@ -1,742 +0,0 @@
-: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set s;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^
-set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @'
-: heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \
-: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \
-: "[Hide @GOTO; Hide =begin; Hide @REM] #not necessary but can help avoid errs in testing" +
-: << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH'
-: STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT - except for first double quoted section.
-: shebang line is not required on unix or windows and will reduce functionality and/or portability.
-: Even comment lines can be part of the functionality of this script (both on unix and windows) - modify with care.
-@GOTO :skip_perl_pod_start ^;
-=begin excludeperl
-: skip_perl_pod_start
-: Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \
-: {
-@REM ############################################################################################################################
-@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, (some sh) and/or powershelll (powershell.exe or pwsh.exe)
-@REM It should remain portable between unix-like OSes & windows if the proper structure is maintained.
-@REM ############################################################################################################################
-@REM On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate.
-@REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system
-@REM e.g from within a running punkshell: deck scriptwrap.multishell -outputfolder
-@REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used)
-@REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh if the script is called without an interpreter - but it may depend on the shell in use when called.
-@REM If you find yourself really wanting/needing to add a shebang line - do so on the basis that the script will exist on unix-like systems only.
-@REM in batch scripts - array syntax with square brackets is a simulation of arrays or associative arrays.
-@REM note that many shells linked as sh do not support substition syntax and may fail - e.g dash etc - generally bash should be used in this context
-@SETLOCAL EnableExtensions EnableDelayedExpansion
-@SET "validshelltypes= powershell______ sh______________ wslbash_________ bash____________ tcl_____________ perl____________"
-@REM for batch - only win32 is relevant - but other scripts on other platforms also parse the nextshell block to determine next shell to launch
-@REM nextshellpath and nextshelltype indices (underscore-padded to 16wide) are "other" plus those returned by Tcl platform pkg e.g win32,linux,freebsd,macosx
-@REM The horrible underscore-padded fixed-widths are to keep the batch labels aligned whilst allowing values to be set
-@REM If more than 32 chars needed for a target, it can still be done but overall script padding may need checking/adjusting
-@REM Supporting more explicit oses than those listed may also require script padding adjustment
-:
-@SET "nextshellpath[win32___________]=tclsh___________________________"
-@SET "nextshelltype[win32___________]=tcl_____________"
-@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________"
-@SET "nextshelltype[dragonflybsd____]=tcl_____________"
-@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________"
-@SET "nextshelltype[freebsd_________]=tcl_____________"
-@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________"
-@SET "nextshelltype[netbsd__________]=tcl_____________"
-@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________"
-@SET "nextshelltype[linux___________]=tcl_____________"
-@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________"
-@SET "nextshelltype[macosx__________]=tcl_____________"
-@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________"
-@SET "nextshelltype[other___________]=tcl_____________"
-:
-@rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable).
-:
-@SET "asadmin=0"
-:
-@REM @ECHO nextshelltype is %nextshelltype[win32___________]%
-@REM @SET "selected_shelltype=%nextshelltype[win32___________]%"
-@SET "selected_shelltype=%nextshelltype[win32___________]%"
-@REM @ECHO selected_shelltype %selected_shelltype%
-@CALL :stringTrimTrailingUnderscores %selected_shelltype% selected_shelltype_trimmed
-@REM @ECHO selected_shelltype_trimmed %selected_shelltype_trimmed%
-@SET "selected_shellpath=%nextshellpath[win32___________]%"
-@CALL :stringTrimTrailingUnderscores %selected_shellpath% selected_shellpath_trimmed
-@CALL SET "keyRemoved=%%validshelltypes:!selected_shelltype!=%%"
-@REM @ECHO keyremoved %keyRemoved%
-@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available
-@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-@REM -- cmd/batch file section (ignored on unix but should be left in place)
-@REM -- This section intended mainly to launch the next shell (and to escalate privileges if necessary)
-@REM -- Avoid customising this if you are not familiar with batch scripting. cmd/batch script can be useful, but is probably the least expressive language and most error prone.
-@REM -- For example - as this file needs to use unix-style lf line-endings - the label scanner is susceptible to the 512Byte boundary issue: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888
-@REM -- This label issue can be triggered/abused in files with crlf line endings too - but it is less likely to happen accidentaly.
-@REm -- See also: https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/4095133#4095133
-@REM ############################################################################################################################
-@REM -- Due to this issue -seemingly trivial edits of the batch file section can break the script! (for Windows anyway)
-@REM -- Even something as simple as adding or removing an @REM
-@REM -- From within punkshell - use:
-@REM -- deck scriptwrap.checkfile
-@REM -- to check your templates or final wrapped scripts for byte boundary issues
-@REM -- It will report any labels that are on boundaries
-@REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets.
-@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using deck scriptwrap.checkfile is still recommended.
-@REM -- Alternatively, as you should do anyway - test the final script on windows
-@REM -- Aside from adding comments/whitespace to tweak the location of labels - you can try duplicating the label (e.g just add the label on a line above) but this is not guaranteed to work in all situations.
-@REM -- '@REM' is a safer comment mechanism than a leading colon - which is used sparingly here.
-@REM -- A colon anywhere in the script that happens to land on a 512 Byte boundary (from file start or from a callsite) could be misinterpreted as a label
-@REM -- It is unknown what versions of cmd interpreters behave this way - and deck scriptwrap.checkfile doesn't check all such boundaries.
-@REm -- For this reason, batch labels should be chosen to be relatively unlikely to collide with other strings in the file, and simple names such as :exit or :end should probably be avoided
-@REM ############################################################################################################################
-@REM -- custom windows payloads should be in powershell,tclsh (or sh/bash if available) code sections
-@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-@SET "winpath=%~dp0"
-@SET "fname=%~nx0"
-@REM @ECHO fname %fname%
-@REM @ECHO winpath %winpath%
-@REM @ECHO commandlineascalled %0
-@REM @ECHO commandlineresolved %~f0
-@CALL :getNormalizedScriptTail nftail
-@REM @ECHO normalizedscripttail %nftail%
-@CALL :getFileTail %0 clinetail
-@REM @ECHO clinetail %clinetail%
-@CALL :stringToUpper %~nx0 capscripttail
-@REM @ECHO capscriptname: %capscripttail%
-
-@IF "%nftail%"=="%capscripttail%" (
- @ECHO forcing asadmin=1 due to file name on filesystem being uppercase
- @SET "asadmin=1"
-) else (
- @CALL :stringToUpper %clinetail% capcmdlinetail
- @REM @ECHO capcmdlinetail !capcmdlinetail!
- IF "%clinetail%"=="!capcmdlinetail!" (
- @ECHO forcing asadmin=1 due to cmdline scriptname in uppercase
- @set "asadmin=1"
- )
-)
-@SET "vbsGetPrivileges=%temp%\punk_bat_elevate_%fname%.vbs"
-@SET arglist=%*
-@SET "qstrippedargs=args%arglist%"
-@SET "qstrippedargs=%qstrippedargs:"=%"
-@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (
- GOTO :gotPrivileges
-)
-@IF !asadmin!==1 (
- net file 1>NUL 2>NUL
- @IF '!errorlevel!'=='0' ( GOTO :gotPrivileges ) else ( GOTO :getPrivileges )
-)
-@REM padding
-@REM padding
-@REM padding
-@REM padding
-@REM padding
-@REM padding
-@REM padding
-@REM padding
-@REM padding
-@REM padding
-@REM padding
-@REM padding
-@GOTO skip_privileges
-:getPrivileges
-@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo PUNK-ELEVATED & shift /1 & goto :gotPrivileges )
-@ECHO Set UAC = CreateObject^("Shell.Application"^) > "%vbsGetPrivileges%"
-@ECHO args = "PUNK-ELEVATED " >> "%vbsGetPrivileges%"
-@ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%"
-@ECHO args = args ^& strArg ^& " " >> "%vbsGetPrivileges%"
-@ECHO Next >> "%vbsGetPrivileges%"
-@ECHO UAC.ShellExecute "%~dp0%~n0%~x0", args, "", "runas", 1 >> "%vbsGetPrivileges%"
-@ECHO Launching script in new windows due to administrator elevation
-@"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %*
-@EXIT /B
-
-:gotPrivileges
-@REM setlocal & pushd .
-@PUSHD .
-@cd /d %~dp0
-@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (
- @DEL "%vbsGetPrivileges%" 1>nul 2>nul
- @SET arglist=%arglist:~14%
-)
-
-:skip_privileges
-@SET need_ps1=0
-@REM we want the ps1 to exist even if the nextshell isn't powershell
-@if not exist "%~dp0%~n0.ps1" (
- @SET need_ps1=1
-) ELSE (
- fc "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >nul || goto different
- @REM @ECHO "files same"
- @SET need_ps1=0
-)
-@GOTO :pscontinue
-:different
-@REM @ECHO "files differ"
-@SET need_ps1=1
-:pscontinue
-@IF !need_ps1!==1 (
- COPY "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >NUL
-)
-@REM avoid using CALL to launch pwsh,tclsh etc - it will intercept some args such as /?
-@IF "%selected_shelltype_trimmed%"=="powershell" (
- REM pws vs powershell hasn't been tested because we didn't need to copy cmd to ps1 this time
- REM test availability of preferred option of powershell7+ pwsh
- pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; write-host "statusmessage: pwsh-found" >NUL
- SET pwshtest_exitcode=!errorlevel!
- REM ECHO pwshtest_exitcode !pwshtest_exitcode!
- REM fallback to powershell if pwsh failed
- IF !pwshtest_exitcode!==0 (
- pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist%
- SET task_exitcode=!errorlevel!
- ) ELSE (
- REM CALL powershell -nop -nol -c write-host powershell-found
- REM powershell -nop -nol -file "%~dp0%~n0.ps1" %*
- powershell -nop -nol -c set-executionpolicy -Scope Process Unrestricted; %~dp0%~n0.ps1" %arglist%
- SET task_exitcode=!errorlevel!
- )
-) ELSE (
- IF "%selected_shelltype_trimmed%"=="wslbash" (
- CALL :getWslPath %winpath% wslpath
- REM ECHO wslfullpath "!wslpath!%fname%"
- %selected_shellpath_trimmed% "!wslpath!%fname%" %arglist%
- SET task_exitcode=!errorlevel!
- ) ELSE (
- REM perl or tcl or sh or bash
- IF NOT "x%keyRemoved%"=="x%validshelltypes%" (
- REM sh on windows uses /c/ instead of /mnt/c - at least if using msys. Todo, review what is the norm on windows with and without msys2,cygwin,wsl
- REM and what logic if any may be needed. For now sh with /c/xxx seems to work the same as sh with c:/xxx
- REM The compound statement with trailing call is required to stop batch termination confirmation, whilst still capturing exitcode
- %selected_shellpath_trimmed% "%~dp0%fname%" %arglist% & SET task_exitcode=!errorlevel! & Call;
- ) ELSE (
- ECHO %fname% has invalid nextshelltype value %selected_shelltype% valid options are %validshelltypes%
- SET task_exitcode=66
- @REM boundary padding
- @REM boundary padding
- @REM boundary padding
- @REM boundary padding
- GOTO :exit_multishell
- )
- )
-)
-@REM batch file library functions
-@REM boundary padding
-@GOTO :endlib
-
-:getWslPath
-@SETLOCAL
- @SET "_path=%~p1"
- @SET "name=%~nx1"
- @SET "drive=%~d1"
- @SET "rtrn=%~2"
- @REM Although drive letters on windows are normally upper case wslbash seems to expect lower case drive letters
- @CALL :stringToLower %drive ldrive
- @SET "result=/mnt/%ldrive:~0,1%%_path:\=/%%name%"
-@ENDLOCAL & (
- @if "%~2" neq "" (
- SET "%rtrn%=%result%"
- ) ELSE (
- ECHO %result%
- )
-)
-@EXIT /B
-
-:getFileTail
-@REM return tail of file without any normalization e.g c:/punkshell/bin/Punk.cmd returns Punk.cmd even if file is punk.cmd
-@REM we can't use things such as %~nx1 as it can change capitalisation
-@REM This function is designed explicitly to preserve capitalisation
-@REM accepts full paths with either / or \ as delimiters - or
-@SETLOCAL
- @SET "rtrn=%~2"
- @SET "arg=%~1"
- @REM @SET "result=%_arg:*/=%"
- @REM @SET "result=%~1"
- @SET LF=^
-
-
- : The above 2 empty lines are important. Don't remove
- @CALL :stringContains "!arg!" "\" hasBackSlash
- @IF "!hasBackslash!"=="true" (
- @for %%A in ("!LF!") do @(
- @FOR /F %%B in ("!arg:\=%%~A!") do @set "result=%%B"
- )
- ) ELSE (
- @CALL :stringContains "!arg!" "/" hasForwardSlash
- @IF "!hasForwardSlash!"=="true" (
- @FOR %%A in ("!LF!") do @(
- @FOR /F %%B in ("!arg:/=%%~A!") do @set "result=%%B"
- )
- ) ELSE (
- @set "result=%arg%"
- )
- )
-@ENDLOCAL & (
- @if "%~2" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO %result%
- )
-)
-@EXIT /B
-@REM boundary padding
-@REM boundary padding
-:getNormalizedScriptTail
-@SETLOCAL
- @SET "result=%~nx0"
- @SET "rtrn=%~1"
-@ENDLOCAL & (
- @IF "%~1" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO %result%
- )
-)
-@EXIT /B
-
-:getNormalizedFileTailFromPath
-@REM warn via echo, and do not set return variable if path not found
-@REM note that %~nx1 does not preserve case of provided path - hence the name 'normalized'
-@REM boundary padding
-@REM boundary padding
-@REM boundary padding
-@REM boundary padding
-@SETLOCAL
- @CALL :stringContains %~1 "\" hasBackSlash
- @CALL :stringContains %~1 "/" hasForwardSlash
- @IF "%hasBackslash%-%hasForwardslash%"=="false-false" (
- @SET "P=%cd%%~1"
- @CALL :getNormalizedFileTailFromPath "!P!" ftail2
- @SET "result=!ftail2!"
- ) else (
- @IF EXIST "%~1" (
- @SET "result=%~nx1"
- ) else (
- @ECHO error getNormalizedFileTailFromPath file not found: %~1
- @EXIT /B 1
- )
- )
- @SET "rtrn=%~2"
-@ENDLOCAL & (
- @IF "%~2" neq "" (
- SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO getNormalizedFileTailFromPath %1 result: %result%
- )
-)
-@EXIT /B
-
-:stringContains
-@REM usage: @CALL:stringContains string needle returnvarname
-@SETLOCAL
- @SET "rtrn=%~3"
- @SET "string=%~1"
- @SET "needle=%~2"
- @IF "!string:%needle%=!"=="!string!" @(
- @SET "result=false"
- ) ELSE (
- @SET "result=true"
- )
-@ENDLOCAL & (
- @IF "%~3" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO stringContains %string% %needle% result: %result%
- )
-)
-@EXIT /B
-@REM boundary padding
-@REM boundary padding
-:stringToUpper
-@SETLOCAL
- @SET "rtrn=%~2"
- @SET "string=%~1"
- @SET "capstring=%~1"
- @FOR %%A in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) DO @(
- @SET "capstring=!capstring:%%A=%%A!"
- )
- @SET "result=!capstring!"
-@ENDLOCAL & (
- @IF "%~2" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO stringToUpper %string% result: %result%
- )
-)
-@EXIT /B
-:stringToLower
-@SETLOCAL
- @SET "rtrn=%~2"
- @SET "string=%~1"
- @SET "retstring=%~1"
- @FOR %%A in (a b c d e f g h i j k l m n o p q r s t u v w x y z) DO @(
- @SET "retstring=!retstring:%%A=%%A!"
- )
- @SET "result=!retstring!"
-@ENDLOCAL & (
- @IF "%~2" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO stringToLower %string% result: %result%
- )
-)
-@EXIT /B
-@REM boundary padding
-@REM boundary padding
-:stringTrimTrailingUnderscores
-@SETLOCAL
- @SET "rtrn=%~2"
- @SET "string=%~1"
- @SET "trimstring=%~1"
- @REM trim up to 31 underscores from the end of a string using string substitution
- @SET trimstring=%trimstring%###
- @SET trimstring=%trimstring:________________###=###%
- @SET trimstring=%trimstring:________###=###%
- @SET trimstring=%trimstring:____###=###%
- @SET trimstring=%trimstring:__###=###%
- @SET trimstring=%trimstring:_###=###%
- @SET trimstring=%trimstring:###=%
- @SET "result=!trimstring!"
-@ENDLOCAL & (
- @IF "%~2" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO stringTrimTrailingUnderscores %string% result: %result%
- )
-)
-@EXIT /B
-:isNumeric
-@SETLOCAL
- @SET "notnumeric="&FOR /F "delims=0123456789" %%i in ("%1") do set "notnumeric=%%i"
- @IF defined notnumeric (
- @SET "result=false"
- ) else (
- @SET "result=true"
- )
- @SET "rtrn=%~2"
-@ENDLOCAL & (
- @IF "%~2" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO %result%
- )
-)
-@EXIT /B
-
-:endlib
-: \
-@REM padding
-@REM padding
-@REM @SET taskexit_code=!errorlevel! & goto :exit_multishell
-@GOTO :exit_multishell
-# }
-# -*- tcl -*-
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- tcl script section
-# -- This is a punk multishell file
-# -- Primary payload target is Tcl, with sh,bash,powershell as helpers
-# -- but it may equally be used with any of these being the primary script.
-# -- It is tuned to run when called as a batch file, a tcl script a sh/bash script or a pwsh/powershell script
-# -- i.e it is a polyglot file.
-# -- The specific layout including some lines that appear just as comments is quite sensitive to change.
-# -- It can be called on unix or windows platforms with or without the interpreter being specified on the commandline.
-# -- e.g ./filename.polypunk.cmd in sh or bash
-# -- e.g tclsh filename.cmd
-# --
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-rename set ""; rename s set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore
-Hide :exit_multishell;Hide {<#};Hide '@
-namespace eval ::punk::multishell {
- set last_script_root [file dirname [file normalize ${::argv0}/__]]
- set last_script [file dirname [file normalize [info script]/__]]
- if {[info exists ::argv0] &&
- $last_script eq $last_script_root
- } {
- set ::punk::multishell::is_main($last_script) 1 ;#run as executable/script - likely desirable to launch application and return an exitcode
- } else {
- set ::punk::multishell::is_main($last_script) 0 ;#sourced - likely to be being used as a library - no launch, no exit. Can use return.
- }
- if {"::punk::multishell::is_main" ni [info commands ::punk::multishell::is_main]} {
- proc ::punk::multishell::is_main {{script_name {}}} {
- if {$script_name eq ""} {
- set script_name [file dirname [file normalize [info script]/--]]
- }
- if {![info exists ::punk::multishell::is_main($script_name)]} {
- #e.g a .dll or something else unanticipated
- puts stderr "Warning punk::multishell didn't recognize info script result: $script_name - will treat as if sourced and return instead of exiting"
- puts stderr "Info: script_root: [file dirname [file normalize ${::argv0}/__]]"
- return 0
- }
- return [set ::punk::multishell::is_main($script_name)]
- }
- }
-}
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload
-#puts "script : [info script]"
-#puts "argcount : $::argc"
-#puts "argvalues: $::argv"
-#puts "argv0 : $::argv0"
-# -- --- --- --- --- --- --- --- --- --- --- ---
-
-
-#
-#
-
-#
-#
-
-
-#
-#
-
-
-# -- --- --- --- --- --- --- --- --- --- --- ---
-# -- Best practice is to always return or exit above, or just by leaving the below defaults in place.
-# -- If the multishell script is modified to have Tcl below the Tcl Payload section,
-# -- then Tcl bracket balancing needs to be carefully managed in the shell and powershell sections below.
-# -- Only the # in front of the two relevant if statements below needs to be removed to enable Tcl below
-# -- but the sh/bash 'then' and 'fi' would also need to be uncommented.
-# -- This facility left in place for experiments on whether configuration payloads etc can be appended
-# -- to tail of file - possibly binary with ctrl-z char - but utility is dependent on which other interpreters/shells
-# -- can be made to ignore/cope with such data.
-if {[::punk::multishell::is_main]} {
- exit 0
-} else {
- return
-}
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload
-# end hide from unix shells \
-HEREDOC1B_HIDE_FROM_BASH_AND_SH
-# sh/bash \
-shift && set -- "${@:1:$#-1}"
-#------------------------------------------------------
-# -- This if block only needed if Tcl didn't exit or return above.
-if false==false # else {
- then
- : #
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- sh/bash script section
-# -- leave as is if all that is required is launching the Tcl payload"
-# --
-# -- Note that sh/bash script isn't called when running a .bat/.cmd from cmd.exe on windows by default
-# -- adjust the %nextshell% value above
-# -- if sh/bash scripting needs to run on windows too.
-# --
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload
-exitcode=0
-#printf "start of bash or sh code"
-
-#
-#
-
-# -- --- --- --- --- --- --- ---
-#
-#-- sh/bash launches Tcl here instead of shebang line at top
-#-- use exec to use exitcode (if any) directly from the tcl script
-#exec /usr/bin/env tclsh "$0" "$@"
-#-- alternative - can run sh/bash script after the tcl call.
-/usr/bin/env tclsh "$0" "$@"
-exitcode=$?
-#echo "sh/bash reporting tcl exitcode: ${exitcode}"
-#-- override exitcode example
-#exit 66
-#
-# -- --- --- --- --- --- --- ---
-
-#
-#
-
-
-#printf "sh/bash done \n"
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end sh Payload
-#------------------------------------------------------
-fi
-exit ${exitcode}
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- Perl script section
-# -- leave the script below as is, if all that is required is launching the Tcl payload"
-# --
-# -- Note that perl script isn't called by default when simply running this script by name
-# -- adjust the nextshell value at the top of the script to point to perl
-# --
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-=cut
-#!/user/bin/perl
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload
-my $exit_code = 0;
-#use ExtUtils::Installed;
-#my $installed = ExtUtils::Installed->new();
-#my @modules = $installed->modules();
-#print "Modules:\n";
-#foreach my $m (@modules) {
-# print "$m\n";
-#}
-# -- --- ---
-
-
-
-my $scriptname = $0;
-print "perl $scriptname\n";
-my $i =1;
-foreach my $a(@ARGV) {
- print "Arg # $i: $a\n";
-}
-
-#
-#
-
-
-
-# -- --- --- --- --- --- --- ---
-#
-$exit_code=system("tclsh", $scriptname, @ARGV);
-#print "perl reporting tcl exitcode: $exit_code";
-#
-# -- --- --- --- --- --- --- ---
-
-#
-#
-
-
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end perl Payload
-exit $exit_code;
-__END__
-
-# end hide sh/bash/perl block from Tcl
-# This comment with closing brace should stay in place whether if commented or not }
-#------------------------------------------------------
-# begin hide powershell-block from Tcl - only needed if Tcl didn't exit or return above
-if 0 {
-: end heredoc1 - end hide from powershell \
-'@
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- powershell/pwsh section
-# -- Do not edit if current file is the .ps1
-# -- Edit the corresponding .cmd and it will autocopy
-# -- unbalanced braces { } here *even in comments* will cause problems if there was no Tcl exit or return above
-# -- custom script should generally go below the begin_powershell_payload line
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-function GetScriptName { $myInvocation.ScriptName }
-$scriptname = GetScriptName
-function GetDynamicParamDictionary {
- [CmdletBinding()]
- param(
- [Parameter(ValueFromPipeline=$true, Mandatory=$true)]
- [string] $CommandName
- )
-
- begin {
- # Get a list of params that should be ignored (they're common to all advanced functions)
- $CommonParameterNames = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([type] [System.Management.Automation.Internal.CommonParameters]) |
- Get-Member -MemberType Properties |
- Select-Object -ExpandProperty Name
- }
-
- process {
- # Create the dictionary that this scriptblock will return:
- $DynParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
-
- # Convert to object array and get rid of Common params:
- (Get-Command $CommandName | select -exp Parameters).GetEnumerator() |
- Where-Object { $CommonParameterNames -notcontains $_.Key } |
- ForEach-Object {
- $DynamicParameter = New-Object System.Management.Automation.RuntimeDefinedParameter (
- $_.Key,
- $_.Value.ParameterType,
- $_.Value.Attributes
- )
- $DynParamDictionary.Add($_.Key, $DynamicParameter)
- }
-
- # Return the dynamic parameters
- return $DynParamDictionary
- }
-}
-# GetDynamicParamDictionary
-# - This can make it easier to share a single set of param definitions between functions
-# - sample usage
-#function ParameterDefinitions {
-# param(
-# [Parameter(Mandatory)][string] $myargument
-# )
-#}
-#function psmain {
-# [CmdletBinding()]
-# param()
-# dynamicparam { GetDynamicParamDictionary ParameterDefinitions }
-# process {
-# #called once with $PSBoundParameters dictionary
-# #can be used to validate arguments, or set a simpler variable name for access
-# switch ($PSBoundParameters.keys) {
-# 'myargumentname' {
-# Set-Variable -Name $_ -Value $PSBoundParameters."$_"
-# }
-# #...
-# }
-# foreach ($boundparam in $PSBoundParameters.GetEnumerator()) {
-# #...
-# }
-# }
-# end {
-# #Main function logic
-# Write-Host "myargumentname value is: $myargumentname"
-# #myotherfunction @PSBoundParameters
-# }
-#}
-#psmain @args
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload
-#"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host
-#"Script Name : {0}" -f $scriptname | write-host
-#"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host
-#"powershell args : {0}" -f ($args -join ", ") | write-host
-# -- --- --- ---
-
-#
-#
-
-
-# -- --- --- --- --- --- --- ---
-#
-tclsh $scriptname $args
-#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host
-#
-# -- --- --- --- --- --- --- ---
-
-
-#
-#
-
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload
-Exit $LASTEXITCODE
-# heredoc2 for powershell to ignore block below
-$1 = @'
-'
-: comment end hide powershell-block from Tcl \
-# This comment with closing brace should stay in place whether 'if' commented or not }
-: multishell doubled-up cmd exit label - return exitcode
-:exit_multishell
-:exit_multishell
-: \
-@REM @ECHO exitcode: !task_exitcode!
-: \
-@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit)
-: \
-@EXIT /B !task_exitcode!
-# cmd has exited
-: comment end heredoc2 \
-'@
-<#
-# id:tailblock0
-# -- powershell multiline comment
-#>
-<#
-no script engine should try to run me
-# id:tailblock1
-#
-
-#
-# -- unreachable by tcl directly if ctrl-z character is in the section above. (but file can be read and split on \x1A)
-# -- Potential for zip and/or base64 contents, but we can't stop pwsh parser from slurping in the data
-# -- so for example a plain text tar archive could cause problems depending on the content.
-# -- final line in file must be the powershell multiline comment terminator or other data it can handle.
-# -- e.g plain # comment lines will work too
-# -- (for example a powershell digital signature is a # commented block of data at the end of the file)
-#>
-
-
diff --git a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell3.cmd b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell3.cmd
deleted file mode 100644
index 17fe4c15..00000000
--- a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell3.cmd
+++ /dev/null
@@ -1,524 +0,0 @@
-: "[rename set s;proc Hide x {proc $x args {}};Hide :]" "\$(function : {<#pwsh#>})" ^
-set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershell;proc Hide x {proc $x args {}}; Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @'
-: heredoc1 - hide from powershell using @ and squote above. (close sqote for unix shells) ' \
-: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl \
-: "[Hide @ECHO; Hide ); Hide (;Hide echo; Hide @REM]#not necessary but can help avoid errs in testing"
-: << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH'
-: Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \
-: {
-: STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT. shebang #! line is not required on unix or windows and will reduce functionality and/or portability.
-: Even comment lines can be part of the functionality of this script (both on unix and windows) - modify with care.
-@REM ############################################################################################################################
-@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, sh and/or powershelll (powershell.exe or pwsh.exe)
-@REM It should remain portable between unix-like OSes & windows if the proper structure is maintained.
-@REM ############################################################################################################################
-@REM On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate.
-@REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system
-@REM e.g from within a running punkshell: pmix scriptwrap.multishell -outputfolder
-@REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used)
-@REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh if the script is called without an interpreter - but it may depend on the shell in use when called.
-@REM If you find yourself really wanting/needing to add a shebang line - do so on the basis that the script will exist on unix-like systems only.
-@SETLOCAL EnableExtensions EnableDelayedExpansion
-@SET "validshells= ^(10^) 'pwsh' ^(11^) 'sh' (^12^) 'bash' (^13^) 'tclsh'"
-@SET "shells[10]=pwsh"
-@SET "shells[11]=sh"
-@set "shells[12]=bash"
-@SET "shells[13]=tclsh"
-:
-@SET "nextshell=13"
-:
-@rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable).
-:
-@SET "asadmin=0"
-:
-@REM nextshell set to index for validshells .eg 10 for pwsh
-@REM @ECHO nextshell is %nextshell%
-@SET "selected=!shells[%nextshell%]!"
-@REM @ECHO selected %selected%
-@CALL SET "keyRemoved=%%validshells:'!selected!'=%%"
-@REM @ECHO keyremoved %keyRemoved%
-@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available
-@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-@REM -- cmd/batch file section (ignored on unix but should be left in place)
-@REM -- This section intended mainly to launch the next shell (and to escalate privileges if necessary)
-@REM -- Avoid customising this if you are not familiar with batch scripting. cmd/batch script can be useful, but is probably the least expressive language and most error prone.
-@REM -- For example - as this file needs to use unix-style lf line-endings - the label scanner is susceptible to the 512Byte boundary issue: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888
-@REM -- This label issue can be triggered/abused in files with crlf line endings too - but it is less likely to happen accidentaly.
-@REm -- See also: https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/4095133#4095133
-@REM ############################################################################################################################
-@REM -- Due to this issue -seemingly trivial edits of the batch file section can break the script! (for Windows anyway)
-@REM -- Even something as simple as adding or removing an @REM
-@REM -- From within punkshell - use:
-@REM -- pmix scriptwrap.checkfile
-@REM -- to check your templates or final wrapped scripts for byte boundary issues
-@REM -- It will report any labels that are on boundaries
-@REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets.
-@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using pmix scriptwrap.checkfile is still recommended.
-@REM -- Alternatively, as you should do anyway - test the final script on windows
-@REM -- Aside from adding comments/whitespace to tweak the location of labels - you can try duplicating the label (e.g just add the label on a line above) but this is not guaranteed to work in all situations.
-@REM -- '@REM' is a safer comment mechanism than a leading colon - which is used sparingly here.
-@REM -- A colon anywhere in the script that happens to land on a 512 Byte boundary (from file start or from a callsite) could be misinterpreted as a label
-@REM -- It is unknown what versions of cmd interpreters behave this way - and pmix scriptwrap.checkfile doesn't check all such boundaries.
-@REm -- For this reason, batch labels should be chosen to be relatively unlikely to collide with other strings in the file, and simple names such as :exit or :end should probably be avoided
-@REM ############################################################################################################################
-@REM -- custom windows payloads should be in powershell,tclsh (or sh/bash if available) code sections
-@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-@SET "winpath=%~dp0"
-@SET "fname=%~nx0"
-@REM @ECHO fname %fname%
-@REM @ECHO winpath %winpath%
-@REM @ECHO commandlineascalled %0
-@REM @ECHO commandlineresolved %~f0
-@CALL :getNormalizedScriptTail nftail
-@REM @ECHO normalizedscripttail %nftail%
-@CALL :getFileTail %0 clinetail
-@REM @ECHO clinetail %clinetail%
-@CALL :stringToUpper %~nx0 capscripttail
-@REM @ECHO capscriptname: %capscripttail%
-
-@IF "%nftail%"=="%capscripttail%" (
- @ECHO forcing asadmin=1 due to file name on filesystem being uppercase
- @SET "asadmin=1"
-) else (
- @CALL :stringToUpper %clinetail% capcmdlinetail
- @REM @ECHO capcmdlinetail !capcmdlinetail!
- IF "%clinetail%"=="!capcmdlinetail!" (
- @ECHO forcing asadmin=1 due to cmdline scriptname in uppercase
- @set "asadmin=1"
- )
-)
-@SET "vbsGetPrivileges=%temp%\punk_bat_elevate_%fname%.vbs"
-@SET arglist=%*
-@IF "%1"=="PUNK-ELEVATED" (
- GOTO :gotPrivileges
-)
-@IF !asadmin!==1 (
- net file 1>NUL 2>NUL
- @IF '!errorlevel!'=='0' ( GOTO :gotPrivileges ) else ( GOTO :getPrivileges )
-)
-@GOTO skip_privileges
-:getPrivileges
-@IF '%1'=='PUNK-ELEVATED' (echo PUNK-ELEVATED & shift /1 & goto :gotPrivileges )
-@ECHO Set UAC = CreateObject^("Shell.Application"^) > "%vbsGetPrivileges%"
-@ECHO args = "PUNK-ELEVATED " >> "%vbsGetPrivileges%"
-@ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%"
-@ECHO args = args ^& strArg ^& " " >> "%vbsGetPrivileges%"
-@ECHO Next >> "%vbsGetPrivileges%"
-@ECHO UAC.ShellExecute "%~dp0%~n0.cmd", args, "", "runas", 1 >> "%vbsGetPrivileges%"
-@ECHO Launching script in new windows due to administrator elevation
-@"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %*
-@EXIT /B
-
-:gotPrivileges
-@REM setlocal & pushd .
-@PUSHD .
-@cd /d %~dp0
-@IF "%1"=="PUNK-ELEVATED" (
- @DEL "%vbsGetPrivileges%" 1>nul 2>nul
- @SET arglist=%arglist:~14%
-)
-
-:skip_privileges
-@SET need_ps1=0
-@REM we want the ps1 to exist even if the nextshell isn't powershell
-@if not exist "%~dp0%~n0.ps1" (
- @SET need_ps1=1
-) ELSE (
- fc "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >nul || goto different
- @REM @ECHO "files same"
- @SET need_ps1=0
-)
-@GOTO :pscontinue
-:different
-@REM @ECHO "files differ"
-@SET need_ps1=1
-:pscontinue
-@IF !need_ps1!==1 (
- COPY "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >NUL
-)
-@REM avoid using CALL to launch pwsh,tclsh etc - it will intercept some args such as /?
-@IF "!shells[%nextshell%]!"=="pwsh" (
- REM pws vs powershell hasn't been tested because we didn't need to copy cmd to ps1 this time
- REM test availability of preferred option of powershell7+ pwsh
- pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; write-host "statusmessage: pwsh-found" >NUL
- SET pwshtest_exitcode=!errorlevel!
- REM ECHO pwshtest_exitcode !pwshtest_exitcode!
- REM fallback to powershell if pwsh failed
- IF !pwshtest_exitcode!==0 (
- pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% & SET task_exitcode=!errorlevel!
- ) ELSE (
- REM CALL powershell -nop -nol -c write-host powershell-found
- REM powershell -nop -nol -file "%~dp0%~n0.ps1" %*
- powershell -nop -nol -c set-executionpolicy -Scope Process Unrestricted; %~dp0%~n0.ps1" %arglist%
- SET task_exitcode=!errorlevel!
- )
-) ELSE (
- IF "!shells[%nextshell%]!"=="bash" (
- CALL :getWslPath %winpath% wslpath
- REM ECHO wslfullpath "!wslpath!%fname%"
- !shells[%nextshell%]! "!wslpath!%fname%" %arglist% & SET task_exitcode=!errorlevel!
- ) ELSE (
- REM probably tclsh or sh
- IF NOT "x%keyRemoved%"=="x%validshells%" (
- REM sh on windows uses /c/ instead of /mnt/c - at least if using msys. Todo, review what is the norm on windows with and without msys2,cygwin,wsl
- REM and what logic if any may be needed. For now sh with /c/xxx seems to work the same as sh with c:/xxx
- !shells[%nextshell%]! "%~dp0%fname%" %arglist% & SET task_exitcode=!errorlevel!
- ) ELSE (
- ECHO %fname% has invalid nextshell value ^(%nextshell%^) !shells[%nextshell%]! valid options are %validshells%
- SET task_exitcode=66
- GOTO :exit_multishell
- )
- )
-)
-@REM batch file library functions
-@GOTO :endlib
-
-:getWslPath
-@SETLOCAL
- @SET "_path=%~p1"
- @SET "name=%~nx1"
- @SET "drive=%~d1"
- @SET "rtrn=%~2"
- @SET "result=/mnt/%drive:~0,1%%_path:\=/%%name%"
-@ENDLOCAL & (
- @if "%~2" neq "" (
- SET "%rtrn%=%result%"
- ) ELSE (
- ECHO %result%
- )
-)
-@EXIT /B
-
-:getFileTail
-@REM return tail of file without any normalization e.g c:/punkshell/bin/Punk.cmd returns Punk.cmd even if file is punk.cmd
-@REM we can't use things such as %~nx1 as it can change capitalisation
-@REM This function is designed explicitly to preserve capitalisation
-@REM accepts full paths with either / or \ as delimiters - or
-@SETLOCAL
- @SET "rtrn=%~2"
- @SET "arg=%~1"
- @REM @SET "result=%_arg:*/=%"
- @REM @SET "result=%~1"
- @SET LF=^
-
-
- : The above 2 empty lines are important. Don't remove
- @CALL :stringContains "!arg!" "\" hasBackSlash
- @IF "!hasBackslash!"=="true" (
- @for %%A in ("!LF!") do @(
- @FOR /F %%B in ("!arg:\=%%~A!") do @set "result=%%B"
- )
- ) ELSE (
- @CALL :stringContains "!arg!" "/" hasForwardSlash
- @IF "!hasForwardSlash!"=="true" (
- @FOR %%A in ("!LF!") do @(
- @FOR /F %%B in ("!arg:/=%%~A!") do @set "result=%%B"
- )
- ) ELSE (
- @set "result=%arg%"
- )
- )
-@ENDLOCAL & (
- @if "%~2" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO %result%
- )
-)
-@EXIT /B
-@REM boundary padding
-:getNormalizedScriptTail
-@SETLOCAL
- @SET "result=%~nx0"
- @SET "rtrn=%~1"
-@ENDLOCAL & (
- @IF "%~1" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO %result%
- )
-)
-@EXIT /B
-
-:getNormalizedFileTailFromPath
-@REM warn via echo, and do not set return variable if path not found
-@REM note that %~nx1 does not preserve case of provided path - hence the name 'normalized'
-@REM boundary padding
-@REM boundary padding
-@SETLOCAL
- @CALL :stringContains %~1 "\" hasBackSlash
- @CALL :stringContains %~1 "/" hasForwardSlash
- @IF "%hasBackslash%-%hasForwardslash%"=="false-false" (
- @SET "P=%cd%%~1"
- @CALL :getNormalizedFileTailFromPath "!P!" ftail2
- @SET "result=!ftail2!"
- ) else (
- @IF EXIST "%~1" (
- @SET "result=%~nx1"
- ) else (
- @ECHO error getNormalizedFileTailFromPath file not found: %~1
- @EXIT /B 1
- )
- )
- @SET "rtrn=%~2"
-@ENDLOCAL & (
- @IF "%~2" neq "" (
- SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO getNormalizedFileTailFromPath %1 result: %result%
- )
-)
-@EXIT /B
-
-:stringContains
-@REM usage: @CALL:stringContains string needle returnvarname
-@SETLOCAL
- @SET "rtrn=%~3"
- @SET "string=%~1"
- @SET "needle=%~2"
- @IF "!string:%needle%=!"=="!string!" @(
- @SET "result=false"
- ) ELSE (
- @SET "result=true"
- )
-@ENDLOCAL & (
- @IF "%~3" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO stringContains %string% %needle% result: %result%
- )
-)
-@EXIT /B
-
-:stringToUpper
-@SETLOCAL
- @SET "rtrn=%~2"
- @SET "string=%~1"
- @SET "capstring=%~1"
- @FOR %%A in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) DO @(
- @SET "capstring=!capstring:%%A=%%A!"
- )
- @SET "result=!capstring!"
-@ENDLOCAL & (
- @IF "%~2" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO stringToUpper %string% result: %result%
- )
-)
-@EXIT /B
-
-:isNumeric
-@SETLOCAL
- @SET "notnumeric="&FOR /F "delims=0123456789" %%i in ("%1") do set "notnumeric=%%i"
- @IF defined notnumeric (
- @SET "result=false"
- ) else (
- @SET "result=true"
- )
- @SET "rtrn=%~2"
-@ENDLOCAL & (
- @IF "%~2" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO %result%
- )
-)
-@EXIT /B
-
-:endlib
-: \
-@REM @SET taskexit_code=!errorlevel! & goto :exit_multishell
-@GOTO :exit_multishell
-# }
-# -*- tcl -*-
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- tcl script section
-# -- This is a punk multishell file
-# -- Primary payload target is Tcl, with sh,bash,powershell as helpers
-# -- but it may equally be used with any of these being the primary script.
-# -- It is tuned to run when called as a batch file, a tcl script a sh/bash script or a pwsh/powershell script
-# -- i.e it is a polyglot file.
-# -- The specific layout including some lines that appear just as comments is quite sensitive to change.
-# -- It can be called on unix or windows platforms with or without the interpreter being specified on the commandline.
-# -- e.g ./filename.polypunk.cmd in sh or bash
-# -- e.g tclsh filename.cmd
-# --
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-rename set ""; rename s set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore
-Hide :exit_multishell;Hide {<#};Hide '@
-namespace eval ::punk::multishell {
- set last_script_root [file dirname [file normalize ${argv0}/__]]
- set last_script [file dirname [file normalize [info script]/__]]
- if {[info exists argv0] &&
- $last_script eq $last_script_root
- } {
- set ::punk::multishell::is_main($last_script) 1 ;#run as executable/script - likely desirable to launch application and return an exitcode
- } else {
- set ::punk::multishell::is_main($last_script) 0 ;#sourced - likely to be being used as a library - no launch, no exit. Can use return.
- }
- if {"::punk::multishell::is_main" ni [info commands ::punk::multishell::is_main]} {
- proc ::punk::multishell::is_main {{script_name {}}} {
- if {$script_name eq ""} {
- set script_name [file dirname [file normalize [info script]/--]]
- }
- if {![info exists ::punk::multishell::is_main($script_name)]} {
- #e.g a .dll or something else unanticipated
- puts stderr "Warning punk::multishell didn't recognize info script result: $script_name - will treat as if sourced and return instead of exiting"
- puts stderr "Info: script_root: [file dirname [file normalize ${argv0}/__]]"
- return 0
- }
- return [set ::punk::multishell::is_main($script_name)]
- }
- }
-}
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload
-#puts "script : [info script]"
-#puts "argcount : $::argc"
-#puts "argvalues: $::argv"
-#puts "argv0 : $::argv0"
-# -- --- --- --- --- --- --- --- --- --- --- ---
-
-
-#
-#
-
-
-
-# -- --- --- --- --- --- --- --- --- --- --- ---
-# -- Best practice is to always return or exit above, or just by leaving the below defaults in place.
-# -- If the multishell script is modified to have Tcl below the Tcl Payload section,
-# -- then Tcl bracket balancing needs to be carefully managed in the shell and powershell sections below.
-# -- Only the # in front of the two relevant if statements below needs to be removed to enable Tcl below
-# -- but the sh/bash 'then' and 'fi' would also need to be uncommented.
-# -- This facility left in place for experiments on whether configuration payloads etc can be appended
-# -- to tail of file - possibly binary with ctrl-z char - but utility is dependent on which other interpreters/shells
-# -- can be made to ignore/cope with such data.
-if {[::punk::multishell::is_main]} {
- exit 0
-} else {
- return
-}
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload
-# end hide from unix shells \
-HEREDOC1B_HIDE_FROM_BASH_AND_SH
-# sh/bash \
-shift && set -- "${@:1:$#-1}"
-#------------------------------------------------------
-# -- This if block only needed if Tcl didn't exit or return above.
-if false==false # else {
- then
- : #
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- sh/bash script section
-# -- leave as is if all that is required is launching the Tcl payload"
-# --
-# -- Note that sh/bash script isn't called when running a .bat/.cmd from cmd.exe on windows by default
-# -- adjust @call line above ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate
-# -- if sh/bash scripting needs to run on windows too.
-# --
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload
-#printf "start of bash or sh code"
-
-#
-#
-
-# -- --- --- --- --- --- --- ---
-#
-exitcode=0 ;#default assumption
-#-- sh/bash launches Tcl here instead of shebang line at top
-#-- use exec to use exitcode (if any) directly from the tcl script
-#exec /usr/bin/env tclsh "$0" "$@"
-#-- alternative - can run sh/bash script after the tcl call.
-/usr/bin/env tclsh "$0" "$@"
-exitcode=$?
-#echo "tcl exitcode: ${exitcode}"
-#-- override exitcode example
-#exit 66
-#
-# -- --- --- --- --- --- --- ---
-
-#
-#
-
-
-#printf "sh/bash done \n"
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end sh Payload
-#------------------------------------------------------
-fi
-exit ${exitcode}
-# end hide sh/bash block from Tcl
-# This comment with closing brace should stay in place whether if commented or not }
-#------------------------------------------------------
-# begin hide powershell-block from Tcl - only needed if Tcl didn't exit or return above
-if 0 {
-: end heredoc1 - end hide from powershell \
-'@
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- powershell/pwsh section
-# -- Do not edit if current file is the .ps1
-# -- Edit the corresponding .cmd and it will autocopy
-# -- unbalanced braces { } here *even in comments* will cause problems if there was no Tcl exit or return above
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-function GetScriptName { $myInvocation.ScriptName }
-$scriptname = getScriptName
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload
-#"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host
-#"Script Name : {0}" -f $scriptname | write-host
-#"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host
-#"powershell args : {0}" -f ($args -join ", ") | write-host
-# -- --- --- ---
-
-#
-#
-
-
-# -- --- --- --- --- --- --- ---
-#
-tclsh $scriptname $args
-#
-# -- --- --- --- --- --- --- ---
-
-
-#
-#
-
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload
-#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host
-Exit $LASTEXITCODE
-# heredoc2 for powershell to ignore block below
-$1 = @'
-'
-: comment end hide powershell-block from Tcl \
-# This comment with closing brace should stay in place whether 'if' commented or not }
-: multishell doubled-up cmd exit label - return exitcode
-:exit_multishell
-:exit_multishell
-: \
-@REM @ECHO exitcode: !task_exitcode!
-: \
-@IF "%1"=="PUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit)
-: \
-@EXIT /B !task_exitcode!
-# cmd has exited
-: comment end heredoc2 \
-'@
-<#
-# id:tailblock0
-# -- powershell multiline comment
-#>
-<#
-# id:tailblock1
-#
-
-#
-# -- unreachable by tcl directly if ctrl-z character is in the section above. (but file can be read and split on \x1A)
-# -- Potential for zip and/or base64 contents, but we can't stop pwsh parser from slurping in the data
-# -- so for example a plain text tar archive could cause problems depending on the content.
-# -- final line in file must be the powershell multiline comment terminator or other data it can handle.
-# -- e.g plain # comment lines will work too
-# -- (for example a powershell digital signature is a # commented block of data at the end of the file)
-#>
-
-
diff --git a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell4.cmd b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell4.cmd
deleted file mode 100644
index a9688b6a..00000000
--- a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell4.cmd
+++ /dev/null
@@ -1,680 +0,0 @@
-: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set s;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^
-set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @'
-: heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \
-: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \
-: "[Hide @GOTO; Hide =begin; Hide @REM] #not necessary but can help avoid errs in testing" +
-: << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH'
-: STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT - except for first double quoted section.
-: shebang line is not required on unix or windows and will reduce functionality and/or portability.
-: Even comment lines can be part of the functionality of this script (both on unix and windows) - modify with care.
-@GOTO :skip_perl_pod_start ^;
-=begin excludeperl
-: skip_perl_pod_start
-: Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \
-: {
-@REM ############################################################################################################################
-@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, sh and/or powershelll (powershell.exe or pwsh.exe)
-@REM It should remain portable between unix-like OSes & windows if the proper structure is maintained.
-@REM ############################################################################################################################
-@REM On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate.
-@REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system
-@REM e.g from within a running punkshell: deck scriptwrap.multishell -outputfolder
-@REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used)
-@REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh if the script is called without an interpreter - but it may depend on the shell in use when called.
-@REM If you find yourself really wanting/needing to add a shebang line - do so on the basis that the script will exist on unix-like systems only.
-@SETLOCAL EnableExtensions EnableDelayedExpansion
-@SET "validshells= ^(10^) 'pwsh' ^(11^) 'sh' (^12^) 'bash' (^13^) 'tclsh' (^14^) 'perl'"
-@SET "shells[10]=pwsh"
-@SET "shells[11]=sh"
-@set "shells[12]=bash"
-@SET "shells[13]=tclsh"
-@SET "shells[14]=perl"
-:
-@SET "nextshell=13"
-:
-@rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable).
-:
-@SET "asadmin=0"
-:
-@REM nextshell set to index for validshells .eg 10 for pwsh
-@REM @ECHO nextshell is %nextshell%
-@SET "selected=!shells[%nextshell%]!"
-@REM @ECHO selected %selected%
-@CALL SET "keyRemoved=%%validshells:'!selected!'=%%"
-@REM @ECHO keyremoved %keyRemoved%
-@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available
-@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-@REM -- cmd/batch file section (ignored on unix but should be left in place)
-@REM -- This section intended mainly to launch the next shell (and to escalate privileges if necessary)
-@REM -- Avoid customising this if you are not familiar with batch scripting. cmd/batch script can be useful, but is probably the least expressive language and most error prone.
-@REM -- For example - as this file needs to use unix-style lf line-endings - the label scanner is susceptible to the 512Byte boundary issue: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888
-@REM -- This label issue can be triggered/abused in files with crlf line endings too - but it is less likely to happen accidentaly.
-@REm -- See also: https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/4095133#4095133
-@REM ############################################################################################################################
-@REM -- Due to this issue -seemingly trivial edits of the batch file section can break the script! (for Windows anyway)
-@REM -- Even something as simple as adding or removing an @REM
-@REM -- From within punkshell - use:
-@REM -- deck scriptwrap.checkfile
-@REM -- to check your templates or final wrapped scripts for byte boundary issues
-@REM -- It will report any labels that are on boundaries
-@REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets.
-@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using deck scriptwrap.checkfile is still recommended.
-@REM -- Alternatively, as you should do anyway - test the final script on windows
-@REM -- Aside from adding comments/whitespace to tweak the location of labels - you can try duplicating the label (e.g just add the label on a line above) but this is not guaranteed to work in all situations.
-@REM -- '@REM' is a safer comment mechanism than a leading colon - which is used sparingly here.
-@REM -- A colon anywhere in the script that happens to land on a 512 Byte boundary (from file start or from a callsite) could be misinterpreted as a label
-@REM -- It is unknown what versions of cmd interpreters behave this way - and deck scriptwrap.checkfile doesn't check all such boundaries.
-@REm -- For this reason, batch labels should be chosen to be relatively unlikely to collide with other strings in the file, and simple names such as :exit or :end should probably be avoided
-@REM ############################################################################################################################
-@REM -- custom windows payloads should be in powershell,tclsh (or sh/bash if available) code sections
-@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-@SET "winpath=%~dp0"
-@SET "fname=%~nx0"
-@REM @ECHO fname %fname%
-@REM @ECHO winpath %winpath%
-@REM @ECHO commandlineascalled %0
-@REM @ECHO commandlineresolved %~f0
-@CALL :getNormalizedScriptTail nftail
-@REM @ECHO normalizedscripttail %nftail%
-@CALL :getFileTail %0 clinetail
-@REM @ECHO clinetail %clinetail%
-@CALL :stringToUpper %~nx0 capscripttail
-@REM @ECHO capscriptname: %capscripttail%
-
-@IF "%nftail%"=="%capscripttail%" (
- @ECHO forcing asadmin=1 due to file name on filesystem being uppercase
- @SET "asadmin=1"
-) else (
- @CALL :stringToUpper %clinetail% capcmdlinetail
- @REM @ECHO capcmdlinetail !capcmdlinetail!
- IF "%clinetail%"=="!capcmdlinetail!" (
- @ECHO forcing asadmin=1 due to cmdline scriptname in uppercase
- @set "asadmin=1"
- )
-)
-@SET "vbsGetPrivileges=%temp%\punk_bat_elevate_%fname%.vbs"
-@SET arglist=%*
-@SET "qstrippedargs=args%arglist%"
-@SET "qstrippedargs=%qstrippedargs:"=%"
-@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (
- GOTO :gotPrivileges
-)
-@IF !asadmin!==1 (
- net file 1>NUL 2>NUL
- @IF '!errorlevel!'=='0' ( GOTO :gotPrivileges ) else ( GOTO :getPrivileges )
-)
-@REM
-@REM
-@REM
-@REM
-@REM
-@REM
-@REM
-@REM
-@REM
-@REM
-@REM
-@REM
-@REM
-@REM
-@REM
-@REM
-@GOTO skip_privileges
-:getPrivileges
-@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo PUNK-ELEVATED & shift /1 & goto :gotPrivileges )
-@ECHO Set UAC = CreateObject^("Shell.Application"^) > "%vbsGetPrivileges%"
-@ECHO args = "PUNK-ELEVATED " >> "%vbsGetPrivileges%"
-@ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%"
-@ECHO args = args ^& strArg ^& " " >> "%vbsGetPrivileges%"
-@ECHO Next >> "%vbsGetPrivileges%"
-@ECHO UAC.ShellExecute "%~dp0%~n0%~x0", args, "", "runas", 1 >> "%vbsGetPrivileges%"
-@ECHO Launching script in new windows due to administrator elevation
-@"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %*
-@EXIT /B
-
-:gotPrivileges
-@REM setlocal & pushd .
-@PUSHD .
-@cd /d %~dp0
-@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (
- @DEL "%vbsGetPrivileges%" 1>nul 2>nul
- @SET arglist=%arglist:~14%
-)
-
-:skip_privileges
-@SET need_ps1=0
-@REM we want the ps1 to exist even if the nextshell isn't powershell
-@if not exist "%~dp0%~n0.ps1" (
- @SET need_ps1=1
-) ELSE (
- fc "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >nul || goto different
- @REM @ECHO "files same"
- @SET need_ps1=0
-)
-@GOTO :pscontinue
-:different
-@REM @ECHO "files differ"
-@SET need_ps1=1
-:pscontinue
-@IF !need_ps1!==1 (
- COPY "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >NUL
-)
-@REM avoid using CALL to launch pwsh,tclsh etc - it will intercept some args such as /?
-@IF "!shells[%nextshell%]!"=="pwsh" (
- REM pws vs powershell hasn't been tested because we didn't need to copy cmd to ps1 this time
- REM test availability of preferred option of powershell7+ pwsh
- pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; write-host "statusmessage: pwsh-found" >NUL
- SET pwshtest_exitcode=!errorlevel!
- REM ECHO pwshtest_exitcode !pwshtest_exitcode!
- REM fallback to powershell if pwsh failed
- IF !pwshtest_exitcode!==0 (
- pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist%
- SET task_exitcode=!errorlevel!
- ) ELSE (
- REM CALL powershell -nop -nol -c write-host powershell-found
- REM powershell -nop -nol -file "%~dp0%~n0.ps1" %*
- powershell -nop -nol -c set-executionpolicy -Scope Process Unrestricted; %~dp0%~n0.ps1" %arglist%
- SET task_exitcode=!errorlevel!
- )
-) ELSE (
- IF "!shells[%nextshell%]!"=="bash" (
- CALL :getWslPath %winpath% wslpath
- REM ECHO wslfullpath "!wslpath!%fname%"
- !shells[%nextshell%]! "!wslpath!%fname%" %arglist%
- SET task_exitcode=!errorlevel!
- ) ELSE (
- REM probably tclsh or sh
- IF NOT "x%keyRemoved%"=="x%validshells%" (
- REM sh on windows uses /c/ instead of /mnt/c - at least if using msys. Todo, review what is the norm on windows with and without msys2,cygwin,wsl
- REM and what logic if any may be needed. For now sh with /c/xxx seems to work the same as sh with c:/xxx
- !shells[%nextshell%]! "%~dp0%fname%" %arglist%
- SET task_exitcode=!errorlevel!
- ) ELSE (
- ECHO %fname% has invalid nextshell value ^(%nextshell%^) !shells[%nextshell%]! valid options are %validshells%
- SET task_exitcode=66
- @REM boundary padding
- @REM boundary padding
- GOTO :exit_multishell
- )
- )
-)
-@REM batch file library functions
-@REM boundary padding
-@GOTO :endlib
-
-:getWslPath
-@SETLOCAL
- @SET "_path=%~p1"
- @SET "name=%~nx1"
- @SET "drive=%~d1"
- @SET "rtrn=%~2"
- @SET "result=/mnt/%drive:~0,1%%_path:\=/%%name%"
-@ENDLOCAL & (
- @if "%~2" neq "" (
- SET "%rtrn%=%result%"
- ) ELSE (
- ECHO %result%
- )
-)
-@EXIT /B
-
-:getFileTail
-@REM return tail of file without any normalization e.g c:/punkshell/bin/Punk.cmd returns Punk.cmd even if file is punk.cmd
-@REM we can't use things such as %~nx1 as it can change capitalisation
-@REM This function is designed explicitly to preserve capitalisation
-@REM accepts full paths with either / or \ as delimiters - or
-@SETLOCAL
- @SET "rtrn=%~2"
- @SET "arg=%~1"
- @REM @SET "result=%_arg:*/=%"
- @REM @SET "result=%~1"
- @SET LF=^
-
-
- : The above 2 empty lines are important. Don't remove
- @CALL :stringContains "!arg!" "\" hasBackSlash
- @IF "!hasBackslash!"=="true" (
- @for %%A in ("!LF!") do @(
- @FOR /F %%B in ("!arg:\=%%~A!") do @set "result=%%B"
- )
- ) ELSE (
- @CALL :stringContains "!arg!" "/" hasForwardSlash
- @IF "!hasForwardSlash!"=="true" (
- @FOR %%A in ("!LF!") do @(
- @FOR /F %%B in ("!arg:/=%%~A!") do @set "result=%%B"
- )
- ) ELSE (
- @set "result=%arg%"
- )
- )
-@ENDLOCAL & (
- @if "%~2" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO %result%
- )
-)
-@EXIT /B
-@REM boundary padding
-@REM boundary padding
-:getNormalizedScriptTail
-@SETLOCAL
- @SET "result=%~nx0"
- @SET "rtrn=%~1"
-@ENDLOCAL & (
- @IF "%~1" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO %result%
- )
-)
-@EXIT /B
-
-:getNormalizedFileTailFromPath
-@REM warn via echo, and do not set return variable if path not found
-@REM note that %~nx1 does not preserve case of provided path - hence the name 'normalized'
-@REM boundary padding
-@REM boundary padding
-@REM boundary padding
-@REM boundary padding
-@SETLOCAL
- @CALL :stringContains %~1 "\" hasBackSlash
- @CALL :stringContains %~1 "/" hasForwardSlash
- @IF "%hasBackslash%-%hasForwardslash%"=="false-false" (
- @SET "P=%cd%%~1"
- @CALL :getNormalizedFileTailFromPath "!P!" ftail2
- @SET "result=!ftail2!"
- ) else (
- @IF EXIST "%~1" (
- @SET "result=%~nx1"
- ) else (
- @ECHO error getNormalizedFileTailFromPath file not found: %~1
- @EXIT /B 1
- )
- )
- @SET "rtrn=%~2"
-@ENDLOCAL & (
- @IF "%~2" neq "" (
- SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO getNormalizedFileTailFromPath %1 result: %result%
- )
-)
-@EXIT /B
-
-:stringContains
-@REM usage: @CALL:stringContains string needle returnvarname
-@SETLOCAL
- @SET "rtrn=%~3"
- @SET "string=%~1"
- @SET "needle=%~2"
- @IF "!string:%needle%=!"=="!string!" @(
- @SET "result=false"
- ) ELSE (
- @SET "result=true"
- )
-@ENDLOCAL & (
- @IF "%~3" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO stringContains %string% %needle% result: %result%
- )
-)
-@EXIT /B
-
-:stringToUpper
-@SETLOCAL
- @SET "rtrn=%~2"
- @SET "string=%~1"
- @SET "capstring=%~1"
- @FOR %%A in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) DO @(
- @SET "capstring=!capstring:%%A=%%A!"
- )
- @SET "result=!capstring!"
-@ENDLOCAL & (
- @IF "%~2" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO stringToUpper %string% result: %result%
- )
-)
-@EXIT /B
-
-:isNumeric
-@SETLOCAL
- @SET "notnumeric="&FOR /F "delims=0123456789" %%i in ("%1") do set "notnumeric=%%i"
- @IF defined notnumeric (
- @SET "result=false"
- ) else (
- @SET "result=true"
- )
- @SET "rtrn=%~2"
-@ENDLOCAL & (
- @IF "%~2" neq "" (
- @SET "%rtrn%=%result%"
- ) ELSE (
- @ECHO %result%
- )
-)
-@EXIT /B
-
-:endlib
-: \
-@REM @SET taskexit_code=!errorlevel! & goto :exit_multishell
-@GOTO :exit_multishell
-# }
-# -*- tcl -*-
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- tcl script section
-# -- This is a punk multishell file
-# -- Primary payload target is Tcl, with sh,bash,powershell as helpers
-# -- but it may equally be used with any of these being the primary script.
-# -- It is tuned to run when called as a batch file, a tcl script a sh/bash script or a pwsh/powershell script
-# -- i.e it is a polyglot file.
-# -- The specific layout including some lines that appear just as comments is quite sensitive to change.
-# -- It can be called on unix or windows platforms with or without the interpreter being specified on the commandline.
-# -- e.g ./filename.polypunk.cmd in sh or bash
-# -- e.g tclsh filename.cmd
-# --
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-rename set ""; rename s set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore
-Hide :exit_multishell;Hide {<#};Hide '@
-namespace eval ::punk::multishell {
- set last_script_root [file dirname [file normalize ${argv0}/__]]
- set last_script [file dirname [file normalize [info script]/__]]
- if {[info exists argv0] &&
- $last_script eq $last_script_root
- } {
- set ::punk::multishell::is_main($last_script) 1 ;#run as executable/script - likely desirable to launch application and return an exitcode
- } else {
- set ::punk::multishell::is_main($last_script) 0 ;#sourced - likely to be being used as a library - no launch, no exit. Can use return.
- }
- if {"::punk::multishell::is_main" ni [info commands ::punk::multishell::is_main]} {
- proc ::punk::multishell::is_main {{script_name {}}} {
- if {$script_name eq ""} {
- set script_name [file dirname [file normalize [info script]/--]]
- }
- if {![info exists ::punk::multishell::is_main($script_name)]} {
- #e.g a .dll or something else unanticipated
- puts stderr "Warning punk::multishell didn't recognize info script result: $script_name - will treat as if sourced and return instead of exiting"
- puts stderr "Info: script_root: [file dirname [file normalize ${argv0}/__]]"
- return 0
- }
- return [set ::punk::multishell::is_main($script_name)]
- }
- }
-}
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload
-#puts "script : [info script]"
-#puts "argcount : $::argc"
-#puts "argvalues: $::argv"
-#puts "argv0 : $::argv0"
-# -- --- --- --- --- --- --- --- --- --- --- ---
-
-
-#
-#
-
-#
-#
-
-
-#
-#
-
-
-# -- --- --- --- --- --- --- --- --- --- --- ---
-# -- Best practice is to always return or exit above, or just by leaving the below defaults in place.
-# -- If the multishell script is modified to have Tcl below the Tcl Payload section,
-# -- then Tcl bracket balancing needs to be carefully managed in the shell and powershell sections below.
-# -- Only the # in front of the two relevant if statements below needs to be removed to enable Tcl below
-# -- but the sh/bash 'then' and 'fi' would also need to be uncommented.
-# -- This facility left in place for experiments on whether configuration payloads etc can be appended
-# -- to tail of file - possibly binary with ctrl-z char - but utility is dependent on which other interpreters/shells
-# -- can be made to ignore/cope with such data.
-if {[::punk::multishell::is_main]} {
- exit 0
-} else {
- return
-}
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload
-# end hide from unix shells \
-HEREDOC1B_HIDE_FROM_BASH_AND_SH
-# sh/bash \
-shift && set -- "${@:1:$#-1}"
-#------------------------------------------------------
-# -- This if block only needed if Tcl didn't exit or return above.
-if false==false # else {
- then
- : #
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- sh/bash script section
-# -- leave as is if all that is required is launching the Tcl payload"
-# --
-# -- Note that sh/bash script isn't called when running a .bat/.cmd from cmd.exe on windows by default
-# -- adjust the %nextshell% value above
-# -- if sh/bash scripting needs to run on windows too.
-# --
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload
-exitcode=0
-#printf "start of bash or sh code"
-
-#
-#
-
-# -- --- --- --- --- --- --- ---
-#
-#-- sh/bash launches Tcl here instead of shebang line at top
-#-- use exec to use exitcode (if any) directly from the tcl script
-#exec /usr/bin/env tclsh "$0" "$@"
-#-- alternative - can run sh/bash script after the tcl call.
-/usr/bin/env tclsh "$0" "$@"
-exitcode=$?
-#echo "sh/bash reporting tcl exitcode: ${exitcode}"
-#-- override exitcode example
-#exit 66
-#
-# -- --- --- --- --- --- --- ---
-
-#
-#
-
-
-#printf "sh/bash done \n"
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end sh Payload
-#------------------------------------------------------
-fi
-exit ${exitcode}
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- Perl script section
-# -- leave the script below as is, if all that is required is launching the Tcl payload"
-# --
-# -- Note that perl script isn't called by default when simply running this script by name
-# -- adjust the nextshell value at the top of the script to point to perl
-# --
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-=cut
-#!/user/bin/perl
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload
-my $exit_code = 0;
-#use ExtUtils::Installed;
-#my $installed = ExtUtils::Installed->new();
-#my @modules = $installed->modules();
-#print "Modules:\n";
-#foreach my $m (@modules) {
-# print "$m\n";
-#}
-# -- --- ---
-
-
-
-my $scriptname = $0;
-print "perl $scriptname\n";
-my $i =1;
-foreach my $a(@ARGV) {
- print "Arg # $i: $a\n";
-}
-
-#
-#
-
-
-
-# -- --- --- --- --- --- --- ---
-#
-$exit_code=system("tclsh", $scriptname, @ARGV);
-#print "perl reporting tcl exitcode: $exit_code";
-#
-# -- --- --- --- --- --- --- ---
-
-#
-#
-
-
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end perl Payload
-exit $exit_code;
-__END__
-
-# end hide sh/bash/perl block from Tcl
-# This comment with closing brace should stay in place whether if commented or not }
-#------------------------------------------------------
-# begin hide powershell-block from Tcl - only needed if Tcl didn't exit or return above
-if 0 {
-: end heredoc1 - end hide from powershell \
-'@
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-# -- powershell/pwsh section
-# -- Do not edit if current file is the .ps1
-# -- Edit the corresponding .cmd and it will autocopy
-# -- unbalanced braces { } here *even in comments* will cause problems if there was no Tcl exit or return above
-# -- custom script should generally go below the begin_powershell_payload line
-# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
-function GetScriptName { $myInvocation.ScriptName }
-$scriptname = GetScriptName
-function GetDynamicParamDictionary {
- [CmdletBinding()]
- param(
- [Parameter(ValueFromPipeline=$true, Mandatory=$true)]
- [string] $CommandName
- )
-
- begin {
- # Get a list of params that should be ignored (they're common to all advanced functions)
- $CommonParameterNames = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([type] [System.Management.Automation.Internal.CommonParameters]) |
- Get-Member -MemberType Properties |
- Select-Object -ExpandProperty Name
- }
-
- process {
- # Create the dictionary that this scriptblock will return:
- $DynParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
-
- # Convert to object array and get rid of Common params:
- (Get-Command $CommandName | select -exp Parameters).GetEnumerator() |
- Where-Object { $CommonParameterNames -notcontains $_.Key } |
- ForEach-Object {
- $DynamicParameter = New-Object System.Management.Automation.RuntimeDefinedParameter (
- $_.Key,
- $_.Value.ParameterType,
- $_.Value.Attributes
- )
- $DynParamDictionary.Add($_.Key, $DynamicParameter)
- }
-
- # Return the dynamic parameters
- return $DynParamDictionary
- }
-}
-# GetDynamicParamDictionary
-# - This can make it easier to share a single set of param definitions between functions
-# - sample usage
-#function ParameterDefinitions {
-# param(
-# [Parameter(Mandatory)][string] $myargument
-# )
-#}
-#function psmain {
-# [CmdletBinding()]
-# param()
-# dynamicparam { GetDynamicParamDictionary ParameterDefinitions }
-# process {
-# #called once with $PSBoundParameters dictionary
-# #can be used to validate arguments, or set a simpler variable name for access
-# switch ($PSBoundParameters.keys) {
-# 'myargumentname' {
-# Set-Variable -Name $_ -Value $PSBoundParameters."$_"
-# }
-# #...
-# }
-# foreach ($boundparam in $PSBoundParameters.GetEnumerator()) {
-# #...
-# }
-# }
-# end {
-# #Main function logic
-# Write-Host "myargumentname value is: $myargumentname"
-# #myotherfunction @PSBoundParameters
-# }
-#}
-#psmain @args
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload
-#"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host
-#"Script Name : {0}" -f $scriptname | write-host
-#"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host
-#"powershell args : {0}" -f ($args -join ", ") | write-host
-# -- --- --- ---
-
-#
-#
-
-
-# -- --- --- --- --- --- --- ---
-#
-tclsh $scriptname $args
-#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host
-#
-# -- --- --- --- --- --- --- ---
-
-
-#
-#
-
-# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload
-Exit $LASTEXITCODE
-# heredoc2 for powershell to ignore block below
-$1 = @'
-'
-: comment end hide powershell-block from Tcl \
-# This comment with closing brace should stay in place whether 'if' commented or not }
-: multishell doubled-up cmd exit label - return exitcode
-:exit_multishell
-:exit_multishell
-: \
-@REM @ECHO exitcode: !task_exitcode!
-: \
-@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit)
-: \
-@EXIT /B !task_exitcode!
-# cmd has exited
-: comment end heredoc2 \
-'@
-<#
-# id:tailblock0
-# -- powershell multiline comment
-#>
-<#
-no script engine should try to run me
-# id:tailblock1
-#
-
-#
-# -- unreachable by tcl directly if ctrl-z character is in the section above. (but file can be read and split on \x1A)
-# -- Potential for zip and/or base64 contents, but we can't stop pwsh parser from slurping in the data
-# -- so for example a plain text tar archive could cause problems depending on the content.
-# -- final line in file must be the powershell multiline comment terminator or other data it can handle.
-# -- e.g plain # comment lines will work too
-# -- (for example a powershell digital signature is a # commented block of data at the end of the file)
-#>
-
-
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argp-0.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argp-0.2.tm
deleted file mode 100644
index 1b1f4b78..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argp-0.2.tm
+++ /dev/null
@@ -1,259 +0,0 @@
-
-# Tcl parser for optional arguments in function calls and
-# commandline arguments
-#
-# (c) 2001 Bastien Chevreux
-
-# Index of exported commands
-# - argp::registerArgs
-# - argp::setArgDefaults
-# - argp::setArgsNeeded
-# - argp::parseArgs
-
-# Internal commands
-# - argp::CheckValues
-
-# See end of file for an example on how to use
-
-package provide argp 0.2
-
-namespace eval argp {
- variable Optstore
- variable Opttypes {
- boolean integer double string
- }
-
- namespace export {[a-z]*}
-}
-
-
-proc argp::registerArgs { func arglist } {
- variable Opttypes
- variable Optstore
-
- set parentns [string range [uplevel 1 [list namespace current]] 2 end]
- if { $parentns != "" } {
- append caller $parentns :: $func
- } else {
- set caller $func
- }
- set cmangled [string map {:: _} $caller]
-
- #puts $parentns
- #puts $caller
- #puts $cmangled
-
- set Optstore(keys,$cmangled) {}
- set Optstore(deflist,$cmangled) {}
- set Optstore(argneeded,$cmangled) {}
-
- foreach arg $arglist {
- foreach {opt type default allowed} $arg {
- set optindex [lsearch -glob $Opttypes $type*]
- if { $optindex < 0} {
- return -code error "$caller, unknown type $type while registering arguments.\nAllowed types: [string trim $Opttypes]"
- }
- set type [lindex $Opttypes $optindex]
-
- lappend Optstore(keys,$cmangled) $opt
- set Optstore(type,$opt,$cmangled) $type
- set Optstore(default,$opt,$cmangled) $default
- set Optstore(allowed,$opt,$cmangled) $allowed
- lappend Optstore(deflist,$cmangled) $opt $default
- }
- }
-
- if { [catch {CheckValues $caller $cmangled $Optstore(deflist,$cmangled)} res]} {
- return -code error "Error in declaration of optional arguments.\n$res"
- }
-}
-
-proc argp::setArgDefaults { func arglist } {
- variable Optstore
-
- set parentns [string range [uplevel 1 [list namespace current]] 2 end]
- if { $parentns != "" } {
- append caller $parentns :: $func
- } else {
- set caller $func
- }
- set cmangled [string map {:: _} $caller]
-
- if {![info exists Optstore(deflist,$cmangled)]} {
- return -code error "Arguments for $caller not registered yet."
- }
- set Optstore(deflist,$cmangled) {}
- foreach {opt default} $arglist {
- if {![info exists Optstore(default,$opt,$cmangled)]} {
- return -code error "$caller, unknown option $opt, must be one of: $Optstore(keys,$cmangled)"
- }
- set Optstore(default,$opt,$cmangled) $default
- }
-
- # set the new defaultlist
- foreach opt $Optstore(keys,$cmangled) {
- lappend Optstore(deflist,$cmangled) $opt $Optstore(default,$opt,$cmangled)
- }
-}
-
-proc argp::setArgsNeeded { func arglist } {
- variable Optstore
-
- set parentns [string range [uplevel 1 [list namespace current]] 2 end]
- if { $parentns != "" } {
- append caller $parentns :: $func
- } else {
- set caller $func
- }
- set cmangled [string map {:: _} $caller]
-
- #append caller $parentns :: $func
- #set cmangled ${parentns}_$func
-
- if {![info exists Optstore(deflist,$cmangled)]} {
- return -code error "Arguments for $caller not registered yet."
- }
-
- set Optstore(argneeded,$cmangled) {}
- foreach opt $arglist {
- if {![info exists Optstore(default,$opt,$cmangled)]} {
- return -code error "$caller, unknown option $opt, must be one of: $Optstore(keys,$cmangled)"
- }
- lappend Optstore(argneeded,$cmangled) $opt
- }
-}
-
-
-proc argp::parseArgs { args } {
- variable Optstore
-
- if {[llength $args] == 0} {
- upvar args a opts o
- } else {
- upvar args a [lindex $args 0] o
- }
-
- if { [ catch { set caller [lindex [info level -1] 0]}]} {
- set caller "main program"
- set cmangled ""
- } else {
- set cmangled [string map {:: _} $caller]
- }
-
- if {![info exists Optstore(deflist,$cmangled)]} {
- return -code error "Arguments for $caller not registered yet."
- }
-
- # set the defaults
- array set o $Optstore(deflist,$cmangled)
-
- # but unset the needed arguments
- foreach key $Optstore(argneeded,$cmangled) {
- catch { unset o($key) }
- }
-
- foreach {key val} $a {
- if {![info exists Optstore(type,$key,$cmangled)]} {
- return -code error "$caller, unknown option $key, must be one of: $Optstore(keys,$cmangled)"
- }
- switch -exact -- $Optstore(type,$key,$cmangled) {
- boolean -
- integer {
- if { $val == "" } {
- return -code error "$caller, $key empty string is not $Optstore(type,$key,$cmangled) value."
- }
- if { ![string is $Optstore(type,$key,$cmangled) $val]} {
- return -code error "$caller, $key $val is not $Optstore(type,$key,$cmangled) value."
- }
- }
- double {
- if { $val == "" } {
- return -code error "$caller, $key empty string is not double value."
- }
- if { ![string is double $val]} {
- return -code error "$caller, $key $val is not double value."
- }
- if { [string is integer $val]} {
- set val [expr {$val + .0}]
- }
- }
- default {
- }
- }
- set o($key) $val
- }
-
- foreach key $Optstore(argneeded,$cmangled) {
- if {![info exists o($key)]} {
- return -code error "$caller, needed argument $key was not given."
- }
- }
-
- if { [catch { CheckValues $caller $cmangled [array get o]} err]} {
- return -code error $err
- }
-
- return
-}
-
-
-proc argp::CheckValues { caller cmangled checklist } {
- variable Optstore
-
- #puts "Checking $checklist"
-
- foreach {key val} $checklist {
- if { [llength $Optstore(allowed,$key,$cmangled)] > 0 } {
- switch -exact -- $Optstore(type,$key,$cmangled) {
- string {
- if { [lsearch $Optstore(allowed,$key,$cmangled) $val] < 0} {
- return -code error "$caller, $key $val is not in allowed values: $Optstore(allowed,$key,$cmangled)"
- }
- }
- double -
- integer {
- set found 0
- foreach range $Optstore(allowed,$key,$cmangled) {
- if {[llength $range] == 1} {
- if { $val == [lindex $range 0] } {
- set found 1
- break
- }
- } elseif {[llength $range] == 2} {
- set low [lindex $range 0]
- set high [lindex $range 1]
-
- if { ![string is integer $low] \
- && [string compare "-" $low] != 0} {
- return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has a lower value range that is not integer and not \u00b4-\u00b4: $range"
- }
- if { ![string is integer $high] \
- && [string compare "+" $high] != 0} {
- return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has a upper value range that is not integer and not \u00b4+\u00b4: $range"
- }
- if {[string compare "-" $low] == 0} {
- if { [string compare "+" $high] == 0 \
- || $val <= $high } {
- set found 1
- break
- }
- }
- if { $val >= $low } {
- if {[string compare "+" $high] == 0 \
- || $val <= $high } {
- set found 1
- break
- }
- }
- } else {
- return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has an allowed value range containing more than 2 elements: $range"
- }
- }
- if { $found == 0 } {
- return -code error "$caller, $key $val is not covered by allowed ranges: $Optstore(allowed,$key,$cmangled)"
- }
- }
- }
- }
- }
-}
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argparsingtest-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argparsingtest-0.1.0.tm
deleted file mode 100644
index b97d1b4e..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argparsingtest-0.1.0.tm
+++ /dev/null
@@ -1,601 +0,0 @@
-# -*- tcl -*-
-# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from -buildversion.txt
-# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm
-#
-# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
-# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# (C) Julian Noble 2024
-#
-# @@ Meta Begin
-# Application argparsingtest 0.1.0
-# Meta platform tcl
-# Meta license MIT
-# @@ Meta End
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# doctools header
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-#*** !doctools
-#[manpage_begin punkshell_module_argparsingtest 0 0.1.0]
-#[copyright "2024"]
-#[titledesc {Module API}] [comment {-- Name section and table of contents description --}]
-#[moddesc {-}] [comment {-- Description at end of page heading --}]
-#[require argparsingtest]
-#[keywords module]
-#[description]
-#[para] -
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-#*** !doctools
-#[section Overview]
-#[para] overview of argparsingtest
-#[subsection Concepts]
-#[para] -
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Requirements
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-#*** !doctools
-#[subsection dependencies]
-#[para] packages used by argparsingtest
-#[list_begin itemized]
-
-package require Tcl 8.6-
-package require punk::args
-package require struct::set
-#*** !doctools
-#[item] [package {Tcl 8.6}]
-#[item] [package {punk::args}]
-
-# #package require frobz
-# #*** !doctools
-# #[item] [package {frobz}]
-
-#*** !doctools
-#[list_end]
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-#*** !doctools
-#[section API]
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# oo::class namespace
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-namespace eval argparsingtest::class {
- #*** !doctools
- #[subsection {Namespace argparsingtest::class}]
- #[para] class definitions
- if {[info commands [namespace current]::interface_sample1] eq ""} {
- #*** !doctools
- #[list_begin enumerated]
-
- # oo::class create interface_sample1 {
- # #*** !doctools
- # #[enum] CLASS [class interface_sample1]
- # #[list_begin definitions]
-
- # method test {arg1} {
- # #*** !doctools
- # #[call class::interface_sample1 [method test] [arg arg1]]
- # #[para] test method
- # puts "test: $arg1"
- # }
-
- # #*** !doctools
- # #[list_end] [comment {-- end definitions interface_sample1}]
- # }
-
- #*** !doctools
- #[list_end] [comment {--- end class enumeration ---}]
- }
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# Base namespace
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-namespace eval argparsingtest {
- namespace export {[a-z]*} ;# Convention: export all lowercase
- #variable xyz
-
- #*** !doctools
- #[subsection {Namespace argparsingtest}]
- #[para] Core API functions for argparsingtest
- #[list_begin definitions]
-
- proc test1_ni {args} {
- set defaults [dict create\
- -return string\
- -frametype \uFFEF\
- -show_edge \uFFEF\
- -show_seps \uFFEF\
- -x ""\
- -y b\
- -z c\
- -1 1\
- -2 2\
- -3 3\
- ]
- foreach {k v} $args {
- if {$k ni [dict keys $defaults]} {
- error "unrecognised option '$k'. Known options [dict keys $defaults]"
- }
- }
- set opts [dict merge $defaults $args]
- }
- proc test1_switchmerge {args} {
- set defaults [dict create\
- -return string\
- -frametype \uFFEF\
- -show_edge \uFFEF\
- -show_seps \uFFEF\
- -x ""\
- -y b\
- -z c\
- -1 1\
- -2 2\
- -3 3\
- ]
- foreach {k v} $args {
- switch -- $k {
- -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 {}
- default {
- error "unrecognised option '$k'. Known options [dict keys $defaults]"
- }
- }
- }
- set opts [dict merge $defaults $args]
- }
- #if we need to loop to test arg validity anyway - then dict set as we go is slightly faster than a dict merge at the end
- proc test1_switch {args} {
- set opts [dict create\
- -return string\
- -frametype \uFFEF\
- -show_edge \uFFEF\
- -show_seps \uFFEF\
- -x ""\
- -y b\
- -z c\
- -1 1\
- -2 2\
- -3 3\
- ]
- foreach {k v} $args {
- switch -- $k {
- -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 {
- dict set opts $k $v
- }
- default {
- error "unrecognised option '$k'. Known options [dict keys $opts]"
- }
- }
- }
- return $opts
- }
- variable switchopts
- set switchopts [dict create\
- -return string\
- -frametype \uFFEF\
- -show_edge \uFFEF\
- -show_seps \uFFEF\
- -x ""\
- -y b\
- -z c\
- -1 1\
- -2 2\
- -3 3\
- ]
- #slightly slower than just creating the dict within the proc
- proc test1_switch_nsvar {args} {
- variable switchopts
- set opts $switchopts
- foreach {k v} $args {
- switch -- $k {
- -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 {
- dict set opts $k $v
- }
- default {
- error "unrecognised option '$k'. Known options [dict keys $opts]"
- }
- }
- }
- return $opts
- }
- proc test1_switch2 {args} {
- set opts [dict create\
- -return string\
- -frametype \uFFEF\
- -show_edge \uFFEF\
- -show_seps \uFFEF\
- -x ""\
- -y b\
- -z c\
- -1 1\
- -2 2\
- -3 3\
- ]
- set switches [lmap v [dict keys $opts] {list $v -}]
- set switches [concat {*}$switches]
- set switches [lrange $switches 0 end-1]
- foreach {k v} $args {
- switch -- $k\
- {*}$switches {
- dict set opts $k $v
- }\
- default {
- error "unrecognised option '$k'. Known options [dict keys $opts]"
- }
- }
- return $opts
- }
- proc test1_prefix {args} {
- set opts [dict create\
- -return string\
- -frametype \uFFEF\
- -show_edge \uFFEF\
- -show_seps \uFFEF\
- -x ""\
- -y b\
- -z c\
- -1 1\
- -2 2\
- -3 3\
- ]
- foreach {k v} $args {
- dict set opts [tcl::prefix::match -message "test1_prefix option $k" {-return -frametype -show_edge -show_seps -x -y -z -1 -2 -3} $k] $v
- }
- return $opts
- }
- proc test1_prefix2 {args} {
- set opts [dict create\
- -return string\
- -frametype \uFFEF\
- -show_edge \uFFEF\
- -show_seps \uFFEF\
- -x ""\
- -y b\
- -z c\
- -1 1\
- -2 2\
- -3 3\
- ]
- if {[llength $args]} {
- set knownflags [dict keys $opts]
- }
- foreach {k v} $args {
- dict set opts [tcl::prefix::match -message "test1_prefix2 option $k" $knownflags $k] $v
- }
- return $opts
- }
-
- #punk::args is slower than argp - but comparable, and argp doesn't support solo flags
- proc test1_punkargs {args} {
- set argd [punk::args::parse $args withdef {
- @id -id ::argparsingtest::test1_punkargs
- @cmd -name argtest4 -help "test of punk::args::parse comparative performance"
- @opts -anyopts 0
- -return -default string -type string
- -frametype -default \uFFEF -type string
- -show_edge -default \uFFEF -type string
- -show_seps -default \uFFEF -type string
- -join -type none -multiple 1
- -x -default "" -type string
- -y -default b -type string
- -z -default c -type string
- -1 -default 1 -type boolean
- -2 -default 2 -type integer
- -3 -default 3 -type integer
- @values
- }]
- return [tcl::dict::get $argd opts]
- }
-
- punk::args::define {
- @id -id ::test1_punkargs_by_id
- @cmd -name argtest4 -help "test of punk::args::parse comparative performance"
- @opts -anyopts 0
- -return -default string -type string
- -frametype -default \uFFEF -type string
- -show_edge -default \uFFEF -type string
- -show_seps -default \uFFEF -type string
- -join -type none -multiple 1
- -x -default "" -type string
- -y -default b -type string
- -z -default c -type string
- -1 -default 1 -type boolean
- -2 -default 2 -type integer
- -3 -default 3 -type integer
- @values
- }
- proc test1_punkargs_by_id {args} {
- set argd [punk::args::get_by_id ::test1_punkargs_by_id $args]
- return [tcl::dict::get $argd opts]
- }
-
- punk::args::define {
- @id -id ::argparsingtest::test1_punkargs2
- @cmd -name argtest4 -help "test of punk::args::parse comparative performance"
- @leaders -min 0 -max 0
- @opts -anyopts 0
- -return -default string -type string
- -frametype -default \uFFEF -type string
- -show_edge -default \uFFEF -type string
- -show_seps -default \uFFEF -type string
- -join -type none -multiple 1
- -x -default "" -type string
- -y -default b -type string
- -z -default c -type string
- -1 -default 1 -type boolean
- -2 -default 2 -type integer
- -3 -default 3 -type integer
- @values -min 0 -max 0
- }
- proc test1_punkargs2 {args} {
- set argd [punk::args::parse $args withid ::argparsingtest::test1_punkargs2]
- return [tcl::dict::get $argd opts]
- }
-
-
- proc test1_punkargs_validate_ansistripped {args} {
- set argd [punk::args::parse $args withdef {
- @id -id ::argparsingtest::test1_punkargs_validate_ansistripped
- @cmd -name argtest4 -help "test of punk::args::parse comparative performance"
- @opts -anyopts 0
- -return -default string -type string -choices {string object} -help "return type"
- -frametype -default \uFFEF -type string
- -show_edge -default \uFFEF -type string
- -show_seps -default \uFFEF -type string
- -join -type none -multiple 1
- -x -default "" -type string
- -y -default b -type string
- -z -default c -type string
- -1 -default 1 -type boolean -validate_ansistripped true
- -2 -default 2 -type integer -validate_ansistripped true
- -3 -default 3 -type integer -validate_ansistripped true
- @values
- }]
- return [tcl::dict::get $argd opts]
- }
-
- package require opt
- variable optlist
- tcl::OptProc test1_opt {
- {-return string "return type"}
- {-frametype \uFFEF "type of frame"}
- {-show_edge \uFFEF "show table outer borders"}
- {-show_seps \uFFEF "show separators"}
- {-join "solo option"}
- {-x "" "x val"}
- {-y b "y val"}
- {-z c "z val"}
- {-1 1 "1val"}
- {-2 -int 2 "2val"}
- {-3 -int 3 "3val"}
- } {
- set opts [dict create]
- foreach v [info locals] {
- dict set opts $v [set $v]
- }
- return $opts
- }
-
- package require cmdline
- #cmdline::getoptions is much faster than typedGetoptions
- proc test1_cmdline_untyped {args} {
- set cmdlineopts_untyped {
- {return.arg "string" "return val"}
- {frametype.arg \uFFEF "frame type"}
- {show_edge.arg \uFFEF "show table borders"}
- {show_seps.arg \uFFEF "show table seps"}
- {join "join the things"}
- {x.arg "" "arg x"}
- {y.arg b "arg y"}
- {z.arg c "arg z"}
- {1.arg 1 "arg 1"}
- {2.arg 2 "arg 2"}
- {3.arg 3 "arg 3"}
- }
-
- set usage "usage etc"
- return [::cmdline::getoptions args $cmdlineopts_untyped $usage]
- }
- proc test1_cmdline_typed {args} {
- set cmdlineopts_typed {
- {return.arg "string" "return val"}
- {frametype.arg \uFFEF "frame type"}
- {show_edge.arg \uFFEF "show table borders"}
- {show_seps.arg \uFFEF "show table seps"}
- {join "join the things"}
- {x.arg "" "arg x"}
- {y.arg b "arg y"}
- {z.arg c "arg z"}
- {1.boolean 1 "arg 1"}
- {2.integer 2 "arg 2"}
- {3.integer 3 "arg 3"}
- }
-
- set usage "usage etc"
- return [::cmdline::typedGetoptions args $cmdlineopts_typed $usage]
- }
-
- catch {
- package require argp
- argp::registerArgs test1_argp {
- { -return string "string" }
- { -frametype string \uFFEF }
- { -show_edge string \uFFEF }
- { -show_seps string \uFFEF }
- { -x string "" }
- { -y string b }
- { -z string c }
- { -1 boolean 1 }
- { -2 integer 2 }
- { -3 integer 3 }
- }
- }
- proc test1_argp {args} {
- argp::parseArgs opts
- return [array get opts]
- }
-
- package require tepam
- tepam::procedure {test1_tepam} {
- -args {
- {-return -type string -default string}
- {-frametype -type string -default \uFFEF}
- {-show_edge -type string -default \uFFEF}
- {-show_seps -type string -default \uFFEF}
- {-join -type none -multiple}
- {-x -type string -default ""}
- {-y -type string -default b}
- {-z -type string -default c}
- {-1 -type boolean -default 1}
- {-2 -type integer -default 2}
- {-3 -type integer -default 3}
- }
- } {
- return [dict create return $return frametype $frametype show_edge $show_edge show_seps $show_seps x $x y $y z $z 1 $1 2 $2 3 $3 join $join]
- }
-
- #multiline values use first line of each record to determine amount of indent to trim
- proc test_multiline {args} {
- set t3 [textblock::frame t3]
- set argd [punk::args::parse $args withdef [subst {
- -template1 -default {
- ******
- * t1 *
- ******
- }
- -template2 -default { ------
- ******
- * t2 *
- ******}
- -template3 -default {$t3}
- #substituted or literal values with newlines - no autoindent applied - caller will have to pad appropriately
- -template3b -default {
- $t3
- -----------------
- $t3
- abc\ndef
- }
- -template4 -default "******
- * t4 *
- ******"
- -template5 -default "
-
-
- "
- -flag -default 0 -type boolean
- }]]
- return $argd
- }
- proc test_multiline2 {args} {
- set t3 [textblock::frame t3]
- set argd [punk::args::parse $args withdef {
- -template1 -default {
- ******
- * t1 *
- ******
- }
- -template2 -default { ------
- ******
- * t2 *
- ******}
- -template3 -default {$t3}
- #substituted or literal values with newlines - no autoindent applied - caller will have to pad appropriately
- -template3b -default {
- ${$t3}
- -----------------
- ${$t3}
- abc\ndef
- }
- -template4 -default "******
- * t4 *
- ******"
- -template5 -default "
- a
- ${$t3}
- c
- "
- -flag -default 0 -type boolean
- }]
- return $argd
- }
-
- #proc sample1 {p1 n args} {
- # #*** !doctools
- # #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]]
- # #[para]Description of sample1
- # #[para] Arguments:
- # # [list_begin arguments]
- # # [arg_def tring p1] A description of string argument p1.
- # # [arg_def integer n] A description of integer argument n.
- # # [list_end]
- # return "ok"
- #}
-
-
-
-
- #*** !doctools
- #[list_end] [comment {--- end definitions namespace argparsingtest ---}]
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# Secondary API namespace
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-namespace eval argparsingtest::lib {
- namespace export {[a-z]*} ;# Convention: export all lowercase
- namespace path [namespace parent]
- #*** !doctools
- #[subsection {Namespace argparsingtest::lib}]
- #[para] Secondary functions that are part of the API
- #[list_begin definitions]
-
- #proc utility1 {p1 args} {
- # #*** !doctools
- # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]]
- # #[para]Description of utility1
- # return 1
- #}
-
-
-
- #*** !doctools
- #[list_end] [comment {--- end definitions namespace argparsingtest::lib ---}]
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-#*** !doctools
-#[section Internal]
-namespace eval argparsingtest::system {
- #*** !doctools
- #[subsection {Namespace argparsingtest::system}]
- #[para] Internal functions that are not part of the API
-
-
-
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Ready
-package provide argparsingtest [namespace eval argparsingtest {
- variable pkg argparsingtest
- variable version
- set version 0.1.0
-}]
-return
-
-#*** !doctools
-#[manpage_end]
-
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/cksum-1.1.4.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/cksum-1.1.4.tm
deleted file mode 100644
index 0fb17981..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/cksum-1.1.4.tm
+++ /dev/null
@@ -1,200 +0,0 @@
-# cksum.tcl - Copyright (C) 2002 Pat Thoyts
-#
-# Provides a Tcl only implementation of the unix cksum(1) command. This is
-# similar to the sum(1) command but the algorithm is better defined and
-# standardized across multiple platforms by POSIX 1003.2/D11.2
-#
-# This command has been verified against the cksum command from the GNU
-# textutils package version 2.0
-#
-# -------------------------------------------------------------------------
-# See the file "license.terms" for information on usage and redistribution
-# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
-# -------------------------------------------------------------------------
-
-package require Tcl 8.5-; # tcl minimum version
-
-namespace eval ::crc {
- namespace export cksum
-
- variable cksum_tbl [list 0x0 \
- 0x04C11DB7 0x09823B6E 0x0D4326D9 0x130476DC 0x17C56B6B \
- 0x1A864DB2 0x1E475005 0x2608EDB8 0x22C9F00F 0x2F8AD6D6 \
- 0x2B4BCB61 0x350C9B64 0x31CD86D3 0x3C8EA00A 0x384FBDBD \
- 0x4C11DB70 0x48D0C6C7 0x4593E01E 0x4152FDA9 0x5F15ADAC \
- 0x5BD4B01B 0x569796C2 0x52568B75 0x6A1936C8 0x6ED82B7F \
- 0x639B0DA6 0x675A1011 0x791D4014 0x7DDC5DA3 0x709F7B7A \
- 0x745E66CD 0x9823B6E0 0x9CE2AB57 0x91A18D8E 0x95609039 \
- 0x8B27C03C 0x8FE6DD8B 0x82A5FB52 0x8664E6E5 0xBE2B5B58 \
- 0xBAEA46EF 0xB7A96036 0xB3687D81 0xAD2F2D84 0xA9EE3033 \
- 0xA4AD16EA 0xA06C0B5D 0xD4326D90 0xD0F37027 0xDDB056FE \
- 0xD9714B49 0xC7361B4C 0xC3F706FB 0xCEB42022 0xCA753D95 \
- 0xF23A8028 0xF6FB9D9F 0xFBB8BB46 0xFF79A6F1 0xE13EF6F4 \
- 0xE5FFEB43 0xE8BCCD9A 0xEC7DD02D 0x34867077 0x30476DC0 \
- 0x3D044B19 0x39C556AE 0x278206AB 0x23431B1C 0x2E003DC5 \
- 0x2AC12072 0x128E9DCF 0x164F8078 0x1B0CA6A1 0x1FCDBB16 \
- 0x018AEB13 0x054BF6A4 0x0808D07D 0x0CC9CDCA 0x7897AB07 \
- 0x7C56B6B0 0x71159069 0x75D48DDE 0x6B93DDDB 0x6F52C06C \
- 0x6211E6B5 0x66D0FB02 0x5E9F46BF 0x5A5E5B08 0x571D7DD1 \
- 0x53DC6066 0x4D9B3063 0x495A2DD4 0x44190B0D 0x40D816BA \
- 0xACA5C697 0xA864DB20 0xA527FDF9 0xA1E6E04E 0xBFA1B04B \
- 0xBB60ADFC 0xB6238B25 0xB2E29692 0x8AAD2B2F 0x8E6C3698 \
- 0x832F1041 0x87EE0DF6 0x99A95DF3 0x9D684044 0x902B669D \
- 0x94EA7B2A 0xE0B41DE7 0xE4750050 0xE9362689 0xEDF73B3E \
- 0xF3B06B3B 0xF771768C 0xFA325055 0xFEF34DE2 0xC6BCF05F \
- 0xC27DEDE8 0xCF3ECB31 0xCBFFD686 0xD5B88683 0xD1799B34 \
- 0xDC3ABDED 0xD8FBA05A 0x690CE0EE 0x6DCDFD59 0x608EDB80 \
- 0x644FC637 0x7A089632 0x7EC98B85 0x738AAD5C 0x774BB0EB \
- 0x4F040D56 0x4BC510E1 0x46863638 0x42472B8F 0x5C007B8A \
- 0x58C1663D 0x558240E4 0x51435D53 0x251D3B9E 0x21DC2629 \
- 0x2C9F00F0 0x285E1D47 0x36194D42 0x32D850F5 0x3F9B762C \
- 0x3B5A6B9B 0x0315D626 0x07D4CB91 0x0A97ED48 0x0E56F0FF \
- 0x1011A0FA 0x14D0BD4D 0x19939B94 0x1D528623 0xF12F560E \
- 0xF5EE4BB9 0xF8AD6D60 0xFC6C70D7 0xE22B20D2 0xE6EA3D65 \
- 0xEBA91BBC 0xEF68060B 0xD727BBB6 0xD3E6A601 0xDEA580D8 \
- 0xDA649D6F 0xC423CD6A 0xC0E2D0DD 0xCDA1F604 0xC960EBB3 \
- 0xBD3E8D7E 0xB9FF90C9 0xB4BCB610 0xB07DABA7 0xAE3AFBA2 \
- 0xAAFBE615 0xA7B8C0CC 0xA379DD7B 0x9B3660C6 0x9FF77D71 \
- 0x92B45BA8 0x9675461F 0x8832161A 0x8CF30BAD 0x81B02D74 \
- 0x857130C3 0x5D8A9099 0x594B8D2E 0x5408ABF7 0x50C9B640 \
- 0x4E8EE645 0x4A4FFBF2 0x470CDD2B 0x43CDC09C 0x7B827D21 \
- 0x7F436096 0x7200464F 0x76C15BF8 0x68860BFD 0x6C47164A \
- 0x61043093 0x65C52D24 0x119B4BE9 0x155A565E 0x18197087 \
- 0x1CD86D30 0x029F3D35 0x065E2082 0x0B1D065B 0x0FDC1BEC \
- 0x3793A651 0x3352BBE6 0x3E119D3F 0x3AD08088 0x2497D08D \
- 0x2056CD3A 0x2D15EBE3 0x29D4F654 0xC5A92679 0xC1683BCE \
- 0xCC2B1D17 0xC8EA00A0 0xD6AD50A5 0xD26C4D12 0xDF2F6BCB \
- 0xDBEE767C 0xE3A1CBC1 0xE760D676 0xEA23F0AF 0xEEE2ED18 \
- 0xF0A5BD1D 0xF464A0AA 0xF9278673 0xFDE69BC4 0x89B8FD09 \
- 0x8D79E0BE 0x803AC667 0x84FBDBD0 0x9ABC8BD5 0x9E7D9662 \
- 0x933EB0BB 0x97FFAD0C 0xAFB010B1 0xAB710D06 0xA6322BDF \
- 0xA2F33668 0xBCB4666D 0xB8757BDA 0xB5365D03 0xB1F740B4 ]
-
- variable uid
- if {![info exists uid]} {set uid 0}
-}
-
-# crc::CksumInit --
-#
-# Create and initialize a cksum context. This is cleaned up when we
-# call CksumFinal to obtain the result.
-#
-proc ::crc::CksumInit {} {
- variable uid
- set token [namespace current]::[incr uid]
- upvar #0 $token state
- array set state {t 0 l 0}
- return $token
-}
-
-proc ::crc::CksumUpdate {token data} {
- variable cksum_tbl
- upvar #0 $token state
- set t $state(t)
- binary scan $data c* r
- foreach {n} $r {
- set index [expr { (($t >> 24) ^ ($n & 0xFF)) & 0xFF }]
- # Since the introduction of built-in bigInt support with Tcl
- # 8.5, bit-shifting $t to the left no longer overflows,
- # keeping it 32 bits long. The value grows bigger and bigger
- # instead - a severe hit on performance. For this reason we
- # do a bitwise AND against 0xFFFFFFFF at each step to keep the
- # value within limits.
- set t [expr {0xFFFFFFFF & (($t << 8) ^ [lindex $cksum_tbl $index])}]
- incr state(l)
- }
- set state(t) $t
- return
-}
-
-proc ::crc::CksumFinal {token} {
- variable cksum_tbl
- upvar #0 $token state
- set t $state(t)
- for {set i $state(l)} {$i > 0} {set i [expr {$i>>8}]} {
- set index [expr {(($t >> 24) ^ $i) & 0xFF}]
- set t [expr {0xFFFFFFFF & (($t << 8) ^ [lindex $cksum_tbl $index])}]
- }
- unset state
- return [expr {~$t & 0xFFFFFFFF}]
-}
-
-# crc::Pop --
-#
-# Pop the nth element off a list. Used in options processing.
-#
-proc ::crc::Pop {varname {nth 0}} {
- upvar $varname args
- set r [lindex $args $nth]
- set args [lreplace $args $nth $nth]
- return $r
-}
-
-# Description:
-# Provide a Tcl equivalent of the unix cksum(1) command.
-# Options:
-# -filename name - return a checksum for the specified file.
-# -format string - return the checksum using this format string.
-# -chunksize size - set the chunking read size
-#
-proc ::crc::cksum {args} {
- array set opts [list -filename {} -channel {} -chunksize 4096 \
- -format %u -command {}]
- while {[string match -* [set option [lindex $args 0]]]} {
- switch -glob -- $option {
- -file* { set opts(-filename) [Pop args 1] }
- -chan* { set opts(-channel) [Pop args 1] }
- -chunk* { set opts(-chunksize) [Pop args 1] }
- -for* { set opts(-format) [Pop args 1] }
- -command { set opts(-command) [Pop args 1] }
- default {
- if {[llength $args] == 1} { break }
- if {[string compare $option "--"] == 0} { Pop args ; break }
- set err [join [lsort [array names opts -*]] ", "]
- return -code error "bad option \"option\": must be $err"
- }
- }
- Pop args
- }
-
- if {$opts(-filename) != {}} {
- set opts(-channel) [open $opts(-filename) r]
- fconfigure $opts(-channel) -translation binary
- }
-
- if {$opts(-channel) == {}} {
-
- if {[llength $args] != 1} {
- return -code error "wrong # args: should be\
- cksum ?-format string?\
- -channel chan | -filename file | string"
- }
- set tok [CksumInit]
- CksumUpdate $tok [lindex $args 0]
- set r [CksumFinal $tok]
-
- } else {
-
- set tok [CksumInit]
- while {![eof $opts(-channel)]} {
- CksumUpdate $tok [read $opts(-channel) $opts(-chunksize)]
- }
- set r [CksumFinal $tok]
-
- if {$opts(-filename) != {}} {
- close $opts(-channel)
- }
- }
-
- return [format $opts(-format) $r]
-}
-
-# -------------------------------------------------------------------------
-
-package provide cksum 1.1.4
-
-# -------------------------------------------------------------------------
-# Local variables:
-# mode: tcl
-# indent-tabs-mode: nil
-# End:
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/cmdline-1.5.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/cmdline-1.5.2.tm
deleted file mode 100644
index 4e5e1df9..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/cmdline-1.5.2.tm
+++ /dev/null
@@ -1,933 +0,0 @@
-# cmdline.tcl --
-#
-# This package provides a utility for parsing command line
-# arguments that are processed by our various applications.
-# It also includes a utility routine to determine the
-# application name for use in command line errors.
-#
-# Copyright (c) 1998-2000 by Ajuba Solutions.
-# Copyright (c) 2001-2015 by Andreas Kupries .
-# Copyright (c) 2003 by David N. Welton
-# See the file "license.terms" for information on usage and redistribution
-# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
-
-package require Tcl 8.5-
-package provide cmdline 1.5.2
-
-namespace eval ::cmdline {
- namespace export getArgv0 getopt getKnownOpt getfiles getoptions \
- getKnownOptions usage
-}
-
-# ::cmdline::getopt --
-#
-# The cmdline::getopt works in a fashion like the standard
-# C based getopt function. Given an option string and a
-# pointer to an array or args this command will process the
-# first argument and return info on how to proceed.
-#
-# Arguments:
-# argvVar Name of the argv list that you
-# want to process. If options are found the
-# arg list is modified and the processed arguments
-# are removed from the start of the list.
-# optstring A list of command options that the application
-# will accept. If the option ends in ".arg" the
-# getopt routine will use the next argument as
-# an argument to the option. Otherwise the option
-# is a boolean that is set to 1 if present.
-# optVar The variable pointed to by optVar
-# contains the option that was found (without the
-# leading '-' and without the .arg extension).
-# valVar Upon success, the variable pointed to by valVar
-# contains the value for the specified option.
-# This value comes from the command line for .arg
-# options, otherwise the value is 1.
-# If getopt fails, the valVar is filled with an
-# error message.
-#
-# Results:
-# The getopt function returns 1 if an option was found, 0 if no more
-# options were found, and -1 if an error occurred.
-
-proc ::cmdline::getopt {argvVar optstring optVar valVar} {
- upvar 1 $argvVar argsList
- upvar 1 $optVar option
- upvar 1 $valVar value
-
- set result [getKnownOpt argsList $optstring option value]
-
- if {$result < 0} {
- # Collapse unknown-option error into any-other-error result.
- set result -1
- }
- return $result
-}
-
-# ::cmdline::getKnownOpt --
-#
-# The cmdline::getKnownOpt works in a fashion like the standard
-# C based getopt function. Given an option string and a
-# pointer to an array or args this command will process the
-# first argument and return info on how to proceed.
-#
-# Arguments:
-# argvVar Name of the argv list that you
-# want to process. If options are found the
-# arg list is modified and the processed arguments
-# are removed from the start of the list. Note that
-# unknown options and the args that follow them are
-# left in this list.
-# optstring A list of command options that the application
-# will accept. If the option ends in ".arg" the
-# getopt routine will use the next argument as
-# an argument to the option. Otherwise the option
-# is a boolean that is set to 1 if present.
-# optVar The variable pointed to by optVar
-# contains the option that was found (without the
-# leading '-' and without the .arg extension).
-# valVar Upon success, the variable pointed to by valVar
-# contains the value for the specified option.
-# This value comes from the command line for .arg
-# options, otherwise the value is 1.
-# If getopt fails, the valVar is filled with an
-# error message.
-#
-# Results:
-# The getKnownOpt function returns 1 if an option was found,
-# 0 if no more options were found, -1 if an unknown option was
-# encountered, and -2 if any other error occurred.
-
-proc ::cmdline::getKnownOpt {argvVar optstring optVar valVar} {
- upvar 1 $argvVar argsList
- upvar 1 $optVar option
- upvar 1 $valVar value
-
- # default settings for a normal return
- set value ""
- set option ""
- set result 0
-
- # check if we're past the end of the args list
- if {[llength $argsList] != 0} {
-
- # if we got -- or an option that doesn't begin with -, return (skipping
- # the --). otherwise process the option arg.
- switch -glob -- [set arg [lindex $argsList 0]] {
- "--" {
- set argsList [lrange $argsList 1 end]
- }
- "--*" -
- "-*" {
- set option [string range $arg 1 end]
- if {[string equal [string range $option 0 0] "-"]} {
- set option [string range $arg 2 end]
- }
-
- # support for format: [-]-option=value
- set idx [string first "=" $option 1]
- if {$idx != -1} {
- set _val [string range $option [expr {$idx+1}] end]
- set option [string range $option 0 [expr {$idx-1}]]
- }
-
- if {[lsearch -exact $optstring $option] != -1} {
- # Booleans are set to 1 when present
- set value 1
- set result 1
- set argsList [lrange $argsList 1 end]
- } elseif {[lsearch -exact $optstring "$option.arg"] != -1} {
- set result 1
- set argsList [lrange $argsList 1 end]
-
- if {[info exists _val]} {
- set value $_val
- } elseif {[llength $argsList]} {
- set value [lindex $argsList 0]
- set argsList [lrange $argsList 1 end]
- } else {
- set value "Option \"$option\" requires an argument"
- set result -2
- }
- } else {
- # Unknown option.
- set value "Illegal option \"-$option\""
- set result -1
- }
- }
- default {
- # Skip ahead
- }
- }
- }
-
- return $result
-}
-
-# ::cmdline::getoptions --
-#
-# Process a set of command line options, filling in defaults
-# for those not specified. This also generates an error message
-# that lists the allowed flags if an incorrect flag is specified.
-#
-# Arguments:
-# argvVar The name of the argument list, typically argv.
-# We remove all known options and their args from it.
-# In other words, after the call to this command the
-# referenced variable contains only the non-options,
-# and unknown options.
-# optlist A list-of-lists where each element specifies an option
-# in the form:
-# (where flag takes no argument)
-# flag comment
-#
-# (or where flag takes an argument)
-# flag default comment
-#
-# If flag ends in ".arg" then the value is taken from the
-# command line. Otherwise it is a boolean and appears in
-# the result if present on the command line. If flag ends
-# in ".secret", it will not be displayed in the usage.
-# usage Text to include in the usage display. Defaults to
-# "options:"
-#
-# Results
-# Name value pairs suitable for using with array set.
-# A modified `argvVar`.
-
-proc ::cmdline::getoptions {argvVar optlist {usage options:}} {
- upvar 1 $argvVar argv
-
- set opts [GetOptionDefaults $optlist result]
-
- set argc [llength $argv]
- while {[set err [getopt argv $opts opt arg]]} {
- if {$err < 0} {
- set result(?) ""
- break
- }
- set result($opt) $arg
- }
- if {[info exist result(?)] || [info exists result(help)]} {
- Error [usage $optlist $usage] USAGE
- }
- return [array get result]
-}
-
-# ::cmdline::getKnownOptions --
-#
-# Process a set of command line options, filling in defaults
-# for those not specified. This ignores unknown flags, but generates
-# an error message that lists the correct usage if a known option
-# is used incorrectly.
-#
-# Arguments:
-# argvVar The name of the argument list, typically argv. This
-# We remove all known options and their args from it.
-# In other words, after the call to this command the
-# referenced variable contains only the non-options,
-# and unknown options.
-# optlist A list-of-lists where each element specifies an option
-# in the form:
-# flag default comment
-# If flag ends in ".arg" then the value is taken from the
-# command line. Otherwise it is a boolean and appears in
-# the result if present on the command line. If flag ends
-# in ".secret", it will not be displayed in the usage.
-# usage Text to include in the usage display. Defaults to
-# "options:"
-#
-# Results
-# Name value pairs suitable for using with array set.
-# A modified `argvVar`.
-
-proc ::cmdline::getKnownOptions {argvVar optlist {usage options:}} {
- upvar 1 $argvVar argv
-
- set opts [GetOptionDefaults $optlist result]
-
- # As we encounter them, keep the unknown options and their
- # arguments in this list. Before we return from this procedure,
- # we'll prepend these args to the argList so that the application
- # doesn't lose them.
-
- set unknownOptions [list]
-
- set argc [llength $argv]
- while {[set err [getKnownOpt argv $opts opt arg]]} {
- if {$err == -1} {
- # Unknown option.
-
- # Skip over any non-option items that follow it.
- # For now, add them to the list of unknownOptions.
- lappend unknownOptions [lindex $argv 0]
- set argv [lrange $argv 1 end]
- while {([llength $argv] != 0) \
- && ![string match "-*" [lindex $argv 0]]} {
- lappend unknownOptions [lindex $argv 0]
- set argv [lrange $argv 1 end]
- }
- } elseif {$err == -2} {
- set result(?) ""
- break
- } else {
- set result($opt) $arg
- }
- }
-
- # Before returning, prepend the any unknown args back onto the
- # argList so that the application doesn't lose them.
- set argv [concat $unknownOptions $argv]
-
- if {[info exist result(?)] || [info exists result(help)]} {
- Error [usage $optlist $usage] USAGE
- }
- return [array get result]
-}
-
-# ::cmdline::GetOptionDefaults --
-#
-# This internal procedure processes the option list (that was passed to
-# the getopt or getKnownOpt procedure). The defaultArray gets an index
-# for each option in the option list, the value of which is the option's
-# default value.
-#
-# Arguments:
-# optlist A list-of-lists where each element specifies an option
-# in the form:
-# flag default comment
-# If flag ends in ".arg" then the value is taken from the
-# command line. Otherwise it is a boolean and appears in
-# the result if present on the command line. If flag ends
-# in ".secret", it will not be displayed in the usage.
-# defaultArrayVar The name of the array in which to put argument defaults.
-#
-# Results
-# Name value pairs suitable for using with array set.
-
-proc ::cmdline::GetOptionDefaults {optlist defaultArrayVar} {
- upvar 1 $defaultArrayVar result
-
- set opts {? help}
- foreach opt $optlist {
- set name [lindex $opt 0]
- if {[regsub -- {\.secret$} $name {} name] == 1} {
- # Need to hide this from the usage display and getopt
- }
- lappend opts $name
- if {[regsub -- {\.arg$} $name {} name] == 1} {
-
- # Set defaults for those that take values.
-
- set default [lindex $opt 1]
- set result($name) $default
- } else {
- # The default for booleans is false
- set result($name) 0
- }
- }
- return $opts
-}
-
-# ::cmdline::usage --
-#
-# Generate an error message that lists the allowed flags.
-#
-# Arguments:
-# optlist As for cmdline::getoptions
-# usage Text to include in the usage display. Defaults to
-# "options:"
-#
-# Results
-# A formatted usage message
-
-proc ::cmdline::usage {optlist {usage {options:}}} {
- set str "[getArgv0] $usage\n"
- set longest 20
- set lines {}
- foreach opt [concat $optlist \
- {{- "Forcibly stop option processing"} {help "Print this message"} {? "Print this message"}}] {
- set name "-[lindex $opt 0]"
- if {[regsub -- {\.secret$} $name {} name] == 1} {
- # Hidden option
- continue
- }
- if {[regsub -- {\.arg$} $name {} name] == 1} {
- append name " value"
- set desc "[lindex $opt 2] <[lindex $opt 1]>"
- } else {
- set desc "[lindex $opt 1]"
- }
- set n [string length $name]
- if {$n > $longest} { set longest $n }
- # max not available before 8.5 - set longest [expr {max($longest, )}]
- lappend lines $name $desc
- }
- foreach {name desc} $lines {
- append str "[string trimright [format " %-*s %s" $longest $name $desc]]\n"
- }
-
- return $str
-}
-
-# ::cmdline::getfiles --
-#
-# Given a list of file arguments from the command line, compute
-# the set of valid files. On windows, file globbing is performed
-# on each argument. On Unix, only file existence is tested. If
-# a file argument produces no valid files, a warning is optionally
-# generated.
-#
-# This code also uses the full path for each file. If not
-# given it prepends [pwd] to the filename. This ensures that
-# these files will never conflict with files in our zip file.
-#
-# Arguments:
-# patterns The file patterns specified by the user.
-# quiet If this flag is set, no warnings will be generated.
-#
-# Results:
-# Returns the list of files that match the input patterns.
-
-proc ::cmdline::getfiles {patterns quiet} {
- set result {}
- if {$::tcl_platform(platform) == "windows"} {
- foreach pattern $patterns {
- set pat [file join $pattern]
- set files [glob -nocomplain -- $pat]
- if {$files == {}} {
- if {! $quiet} {
- puts stdout "warning: no files match \"$pattern\""
- }
- } else {
- foreach file $files {
- lappend result $file
- }
- }
- }
- } else {
- set result $patterns
- }
- set files {}
- foreach file $result {
- # Make file an absolute path so that we will never conflict
- # with files that might be contained in our zip file.
- set fullPath [file join [pwd] $file]
-
- if {[file isfile $fullPath]} {
- lappend files $fullPath
- } elseif {! $quiet} {
- puts stdout "warning: no files match \"$file\""
- }
- }
- return $files
-}
-
-# ::cmdline::getArgv0 --
-#
-# This command returns the "sanitized" version of argv0. It will strip
-# off the leading path and remove the ".bin" extensions that our apps
-# use because they must be wrapped by a shell script.
-#
-# Arguments:
-# None.
-#
-# Results:
-# The application name that can be used in error messages.
-
-proc ::cmdline::getArgv0 {} {
- global argv0
-
- set name [file tail $argv0]
- return [file rootname $name]
-}
-
-##
-# ### ### ### ######### ######### #########
-##
-# Now the typed versions of the above commands.
-##
-# ### ### ### ######### ######### #########
-##
-
-# typedCmdline.tcl --
-#
-# This package provides a utility for parsing typed command
-# line arguments that may be processed by various applications.
-#
-# Copyright (c) 2000 by Ross Palmer Mohn.
-# See the file "license.terms" for information on usage and redistribution
-# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
-#
-# RCS: @(#) $Id: cmdline.tcl,v 1.28 2011/02/23 17:41:52 andreas_kupries Exp $
-
-namespace eval ::cmdline {
- namespace export typedGetopt typedGetoptions typedUsage
-
- # variable cmdline::charclasses --
- #
- # Create regexp list of allowable character classes
- # from "string is" error message.
- #
- # Results:
- # String of character class names separated by "|" characters.
-
- variable charclasses
- #checker exclude badKey
- catch {string is . .} charclasses
- variable dummy
- regexp -- {must be (.+)$} $charclasses dummy charclasses
- regsub -all -- {, (or )?} $charclasses {|} charclasses
- unset dummy
-}
-
-# ::cmdline::typedGetopt --
-#
-# The cmdline::typedGetopt works in a fashion like the standard
-# C based getopt function. Given an option string and a
-# pointer to a list of args this command will process the
-# first argument and return info on how to proceed. In addition,
-# you may specify a type for the argument to each option.
-#
-# Arguments:
-# argvVar Name of the argv list that you want to process.
-# If options are found, the arg list is modified
-# and the processed arguments are removed from the
-# start of the list.
-#
-# optstring A list of command options that the application
-# will accept. If the option ends in ".xxx", where
-# xxx is any valid character class to the tcl
-# command "string is", then typedGetopt routine will
-# use the next argument as a typed argument to the
-# option. The argument must match the specified
-# character classes (e.g. integer, double, boolean,
-# xdigit, etc.). Alternatively, you may specify
-# ".arg" for an untyped argument.
-#
-# optVar Upon success, the variable pointed to by optVar
-# contains the option that was found (without the
-# leading '-' and without the .xxx extension). If
-# typedGetopt fails the variable is set to the empty
-# string. SOMETIMES! Different for each -value!
-#
-# argVar Upon success, the variable pointed to by argVar
-# contains the argument for the specified option.
-# If typedGetopt fails, the variable is filled with
-# an error message.
-#
-# Argument type syntax:
-# Option that takes no argument.
-# foo
-#
-# Option that takes a typeless argument.
-# foo.arg
-#
-# Option that takes a typed argument. Allowable types are all
-# valid character classes to the tcl command "string is".
-# Currently must be one of alnum, alpha, ascii, control,
-# boolean, digit, double, false, graph, integer, lower, print,
-# punct, space, true, upper, wordchar, or xdigit.
-# foo.double
-#
-# Option that takes an argument from a list.
-# foo.(bar|blat)
-#
-# Argument quantifier syntax:
-# Option that takes an optional argument.
-# foo.arg?
-#
-# Option that takes a list of arguments terminated by "--".
-# foo.arg+
-#
-# Option that takes an optional list of arguments terminated by "--".
-# foo.arg*
-#
-# Argument quantifiers work on all argument types, so, for
-# example, the following is a valid option specification.
-# foo.(bar|blat|blah)?
-#
-# Argument syntax miscellany:
-# Options may be specified on the command line using a unique,
-# shortened version of the option name. Given that program foo
-# has an option list of {bar.alpha blah.arg blat.double},
-# "foo -b fob" returns an error, but "foo -ba fob"
-# successfully returns {bar fob}
-#
-# Results:
-# The typedGetopt function returns one of the following:
-# 1 a valid option was found
-# 0 no more options found to process
-# -1 invalid option
-# -2 missing argument to a valid option
-# -3 argument to a valid option does not match type
-#
-# Known Bugs:
-# When using options which include special glob characters,
-# you must use the exact option. Abbreviating it can cause
-# an error in the "cmdline::prefixSearch" procedure.
-
-proc ::cmdline::typedGetopt {argvVar optstring optVar argVar} {
- variable charclasses
-
- upvar $argvVar argsList
-
- upvar $optVar retvar
- upvar $argVar optarg
-
- # default settings for a normal return
- set optarg ""
- set retvar ""
- set retval 0
-
- # check if we're past the end of the args list
- if {[llength $argsList] != 0} {
-
- # if we got -- or an option that doesn't begin with -, return (skipping
- # the --). otherwise process the option arg.
- switch -glob -- [set arg [lindex $argsList 0]] {
- "--" {
- set argsList [lrange $argsList 1 end]
- }
-
- "-*" {
- # Create list of options without their argument extensions
-
- set optstr ""
- foreach str $optstring {
- lappend optstr [file rootname $str]
- }
-
- set _opt [string range $arg 1 end]
-
- set i [prefixSearch $optstr [file rootname $_opt]]
- if {$i != -1} {
- set opt [lindex $optstring $i]
-
- set quantifier "none"
- if {[regexp -- {\.[^.]+([?+*])$} $opt dummy quantifier]} {
- set opt [string range $opt 0 end-1]
- }
-
- if {[string first . $opt] == -1} {
- set retval 1
- set retvar $opt
- set argsList [lrange $argsList 1 end]
-
- } elseif {[regexp -- "\\.(arg|$charclasses)\$" $opt dummy charclass]
- || [regexp -- {\.\(([^)]+)\)} $opt dummy charclass]} {
- if {[string equal arg $charclass]} {
- set type arg
- } elseif {[regexp -- "^($charclasses)\$" $charclass]} {
- set type class
- } else {
- set type oneof
- }
-
- set argsList [lrange $argsList 1 end]
- set opt [file rootname $opt]
-
- while {1} {
- if {[llength $argsList] == 0
- || [string equal "--" [lindex $argsList 0]]} {
- if {[string equal "--" [lindex $argsList 0]]} {
- set argsList [lrange $argsList 1 end]
- }
-
- set oneof ""
- if {$type == "arg"} {
- set charclass an
- } elseif {$type == "oneof"} {
- set oneof ", one of $charclass"
- set charclass an
- }
-
- if {$quantifier == "?"} {
- set retval 1
- set retvar $opt
- set optarg ""
- } elseif {$quantifier == "+"} {
- set retvar $opt
- if {[llength $optarg] < 1} {
- set retval -2
- set optarg "Option requires at least one $charclass argument$oneof -- $opt"
- } else {
- set retval 1
- }
- } elseif {$quantifier == "*"} {
- set retval 1
- set retvar $opt
- } else {
- set optarg "Option requires $charclass argument$oneof -- $opt"
- set retvar $opt
- set retval -2
- }
- set quantifier ""
- } elseif {($type == "arg")
- || (($type == "oneof")
- && [string first "|[lindex $argsList 0]|" "|$charclass|"] != -1)
- || (($type == "class")
- && [string is $charclass [lindex $argsList 0]])} {
- set retval 1
- set retvar $opt
- lappend optarg [lindex $argsList 0]
- set argsList [lrange $argsList 1 end]
- } else {
- set oneof ""
- if {$type == "arg"} {
- set charclass an
- } elseif {$type == "oneof"} {
- set oneof ", one of $charclass"
- set charclass an
- }
- set optarg "Option requires $charclass argument$oneof -- $opt"
- set retvar $opt
- set retval -3
-
- if {$quantifier == "?"} {
- set retval 1
- set optarg ""
- }
- set quantifier ""
- }
- if {![regexp -- {[+*]} $quantifier]} {
- break;
- }
- }
- } else {
- Error \
- "Illegal option type specification: must be one of $charclasses" \
- BAD OPTION TYPE
- }
- } else {
- set optarg "Illegal option -- $_opt"
- set retvar $_opt
- set retval -1
- }
- }
- default {
- # Skip ahead
- }
- }
- }
-
- return $retval
-}
-
-# ::cmdline::typedGetoptions --
-#
-# Process a set of command line options, filling in defaults
-# for those not specified. This also generates an error message
-# that lists the allowed options if an incorrect option is
-# specified.
-#
-# Arguments:
-# argvVar The name of the argument list, typically argv
-# optlist A list-of-lists where each element specifies an option
-# in the form:
-#
-# option default comment
-#
-# Options formatting is as described for the optstring
-# argument of typedGetopt. Default is for optionally
-# specifying a default value. Comment is for optionally
-# specifying a comment for the usage display. The
-# options "--", "-help", and "-?" are automatically included
-# in optlist.
-#
-# Argument syntax miscellany:
-# Options formatting and syntax is as described in typedGetopt.
-# There are two additional suffixes that may be applied when
-# passing options to typedGetoptions.
-#
-# You may add ".multi" as a suffix to any option. For options
-# that take an argument, this means that the option may be used
-# more than once on the command line and that each additional
-# argument will be appended to a list, which is then returned
-# to the application.
-# foo.double.multi
-#
-# If a non-argument option is specified as ".multi", it is
-# toggled on and off for each time it is used on the command
-# line.
-# foo.multi
-#
-# If an option specification does not contain the ".multi"
-# suffix, it is not an error to use an option more than once.
-# In this case, the behavior for options with arguments is that
-# the last argument is the one that will be returned. For
-# options that do not take arguments, using them more than once
-# has no additional effect.
-#
-# Options may also be hidden from the usage display by
-# appending the suffix ".secret" to any option specification.
-# Please note that the ".secret" suffix must be the last suffix,
-# after any argument type specification and ".multi" suffix.
-# foo.xdigit.multi.secret
-#
-# Results
-# Name value pairs suitable for using with array set.
-
-proc ::cmdline::typedGetoptions {argvVar optlist {usage options:}} {
- variable charclasses
-
- upvar 1 $argvVar argv
-
- set opts {? help}
- foreach opt $optlist {
- set name [lindex $opt 0]
- if {[regsub -- {\.secret$} $name {} name] == 1} {
- # Remove this extension before passing to typedGetopt.
- }
- if {[regsub -- {\.multi$} $name {} name] == 1} {
- # Remove this extension before passing to typedGetopt.
-
- regsub -- {\..*$} $name {} temp
- set multi($temp) 1
- }
- lappend opts $name
- if {[regsub -- "\\.(arg|$charclasses|\\(.+).?\$" $name {} name] == 1} {
- # Set defaults for those that take values.
- # Booleans are set just by being present, or not
-
- set dflt [lindex $opt 1]
- if {$dflt != {}} {
- set defaults($name) $dflt
- }
- }
- }
- set argc [llength $argv]
- while {[set err [typedGetopt argv $opts opt arg]]} {
- if {$err == 1} {
- if {[info exists result($opt)]
- && [info exists multi($opt)]} {
- # Toggle boolean options or append new arguments
-
- if {$arg == ""} {
- unset result($opt)
- } else {
- set result($opt) "$result($opt) $arg"
- }
- } else {
- set result($opt) "$arg"
- }
- } elseif {($err == -1) || ($err == -3)} {
- Error [typedUsage $optlist $usage] USAGE
- } elseif {$err == -2 && ![info exists defaults($opt)]} {
- Error [typedUsage $optlist $usage] USAGE
- }
- }
- if {[info exists result(?)] || [info exists result(help)]} {
- Error [typedUsage $optlist $usage] USAGE
- }
- foreach {opt dflt} [array get defaults] {
- if {![info exists result($opt)]} {
- set result($opt) $dflt
- }
- }
- return [array get result]
-}
-
-# ::cmdline::typedUsage --
-#
-# Generate an error message that lists the allowed flags,
-# type of argument taken (if any), default value (if any),
-# and an optional description.
-#
-# Arguments:
-# optlist As for cmdline::typedGetoptions
-#
-# Results
-# A formatted usage message
-
-proc ::cmdline::typedUsage {optlist {usage {options:}}} {
- variable charclasses
-
- set str "[getArgv0] $usage\n"
- set longest 20
- set lines {}
- foreach opt [concat $optlist \
- {{help "Print this message"} {? "Print this message"}}] {
- set name "-[lindex $opt 0]"
- if {[regsub -- {\.secret$} $name {} name] == 1} {
- # Hidden option
- continue
- }
-
- if {[regsub -- {\.multi$} $name {} name] == 1} {
- # Display something about multiple options
- }
-
- if {[regexp -- "\\.(arg|$charclasses)\$" $name dummy charclass] ||
- [regexp -- {\.\(([^)]+)\)} $opt dummy charclass]
- } {
- regsub -- "\\..+\$" $name {} name
- append name " $charclass"
- set desc [lindex $opt 2]
- set default [lindex $opt 1]
- if {$default != ""} {
- append desc " <$default>"
- }
- } else {
- set desc [lindex $opt 1]
- }
- lappend accum $name $desc
- set n [string length $name]
- if {$n > $longest} { set longest $n }
- # max not available before 8.5 - set longest [expr {max($longest, [string length $name])}]
- }
- foreach {name desc} $accum {
- append str "[string trimright [format " %-*s %s" $longest $name $desc]]\n"
- }
- return $str
-}
-
-# ::cmdline::prefixSearch --
-#
-# Search a Tcl list for a pattern; searches first for an exact match,
-# and if that fails, for a unique prefix that matches the pattern
-# (i.e, first "lsearch -exact", then "lsearch -glob $pattern*"
-#
-# Arguments:
-# list list of words
-# pattern word to search for
-#
-# Results:
-# Index of found word is returned. If no exact match or
-# unique short version is found then -1 is returned.
-
-proc ::cmdline::prefixSearch {list pattern} {
- # Check for an exact match
-
- if {[set pos [::lsearch -exact $list $pattern]] > -1} {
- return $pos
- }
-
- # Check for a unique short version
-
- set slist [lsort $list]
- if {[set pos [::lsearch -glob $slist $pattern*]] > -1} {
- # What if there is nothing for the check variable?
-
- set check [lindex $slist [expr {$pos + 1}]]
- if {[string first $pattern $check] != 0} {
- return [::lsearch -exact $list [lindex $slist $pos]]
- }
- }
- return -1
-}
-# ::cmdline::Error --
-#
-# Internal helper to throw errors with a proper error-code attached.
-#
-# Arguments:
-# message text of the error message to throw.
-# args additional parts of the error code to use,
-# with CMDLINE as basic prefix added by this command.
-#
-# Results:
-# An error is thrown, always.
-
-proc ::cmdline::Error {message args} {
- return -code error -errorcode [linsert $args 0 CMDLINE] $message
-}
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/commandstack-0.3.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/commandstack-0.3.tm
deleted file mode 100644
index b2561a20..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/commandstack-0.3.tm
+++ /dev/null
@@ -1,518 +0,0 @@
-
-
-#JMN 2021 - Public Domain
-#cooperative command renaming
-#
-# REVIEW 2024 - code was originally for specific use in packageTrace
-# - code should be reviewed for more generic utility.
-# - API is obscure and undocumented.
-# - unclear if intention was only for builtins
-# - consider use of newer 'info cmdtype' - (but need also support for safe interps)
-# - oo dispatch features may be a better implementation - especially for allowing undoing command renames in the middle of a stack.
-# - document that replacement command should use 'commandstack::get_next_command ' for delegating to command as it was prior to rename
-#changes:
-#2024
-# - mungecommand to support namespaced commands
-# - fix mistake - hardcoded _originalcommand_package -> _originalcommand_
-#2021-09-18
-# - initial version
-# - e.g Support cooperation between packageSuppress and packageTrace which both rename the package command
-# - They need to be able to load and unload in any order.
-#
-
-#strive for no other package dependencies here.
-
-
-namespace eval commandstack {
- variable all_stacks
- variable debug
- set debug 0
- variable known_renamers [list ::packagetrace ::packageSuppress]
- if {![info exists all_stacks]} {
- #don't wipe it
- set all_stacks [dict create]
- }
-}
-
-namespace eval commandstack::util {
- #note - we can't use something like md5 to ID proc body text because we don't want to require additional packages.
- #We could store the full text of the body to compare - but we need to identify magic strings from cooperating packages such as packageTrace
- #A magic comment was chosen as the identifying method.
- #The string IMPLEMENTOR_*! is searched for where the text between _ and ! is the name of the package that implemented the proc.
-
- #return unspecified if the command is a proc with a body but no magic comment ID
- #return unknown if the command doesn't have a proc body to analyze
- #otherwise return the package name identified in the magic comment
- proc get_IMPLEMENTOR {command} {
- #assert - command has already been resolved to a namespace ie fully qualified
- if {[llength [info procs $command]]} {
- #look for *IMPLEMENTOR_*!
- set prefix IMPLEMENTOR_
- set suffix "!"
- set body [uplevel 1 [list info body $command]]
- if {[string match "*$prefix*$suffix*" $body]} {
- set prefixposn [string first "$prefix" $body]
- set pkgposn [expr {$prefixposn + [string length $prefix]}]
- #set suffixposn [string first $suffix [string range $body $pkgposn $pkgposn+60]]
- set suffixposn [string first $suffix $body $pkgposn]
- return [string range $body $pkgposn $suffixposn-1]
- } else {
- return unspecified
- }
- } else {
- if {[info commands tcl::info::cmdtype] ne ""} {
- #tcl9 and maybe some tcl 8.7s ?
- switch -- [tcl::info::cmdtype $command] {
- native {
- return builtin
- }
- default {
- return undetermined
- }
- }
- } else {
- return undetermined
- }
- }
- }
-}
-namespace eval commandstack::renamed_commands {}
-namespace eval commandstack::temp {} ;#where we create proc initially before renaming into place
-
-namespace eval commandstack {
- namespace export {[a-z]*}
- proc help {} {
- return {
-
- }
- }
-
- proc debug {{on_off {}}} {
- variable debug
- if {$on_off eq ""} {
- return $debug
- } else {
- if {[string is boolean -strict $debug]} {
- set debug [expr {$on_off && 1}]
- return $debug
- }
- }
- }
-
- proc get_stack {{command ""}} {
- variable all_stacks
- if {$command eq ""} {
- return $all_stacks
- }
- set command [uplevel 1 [list namespace which $command]]
- if {[dict exists $all_stacks $command]} {
- return [dict get $all_stacks $command]
- } else {
- return [list]
- }
- }
-
- #get the implementation to which the renamer (renamer is usually calling namespace) originally renamed it, or the implementation it now points to.
- #review - performance impact. Possible to use oo for faster dispatch whilst allowing stack re-orgs?
- #e.g if renaming builtin 'package' - this command is generally called 'a lot'
- proc get_next_command {command renamer tokenid} {
- variable all_stacks
- if {[dict exists $all_stacks $command]} {
- set stack [dict get $all_stacks $command]
- #stack is a list of dicts, 1st entry is token {}
- set posn [lsearch -index 1 $stack [list $command $renamer $tokenid]]
- if {$posn > -1} {
- set record [lindex $stack $posn]
- return [dict get $record implementation]
- } else {
- error "(commandstack::get_next_command) ERROR: unable to determine next command for '$command' using token: $command $renamer $tokenid"
- }
- } else {
- return $command
- }
- }
- proc basecall {command args} {
- variable all_stacks
- set command [uplevel 1 [list namespace which $command]]
- if {[dict exists $all_stacks $command]} {
- set stack [dict get $all_stacks $command]
- if {[llength $stack]} {
- set rec1 [lindex $stack 0]
- tailcall [dict get $rec1 implementation] {*}$args
- } else {
- tailcall $command {*}$args
- }
- } else {
- tailcall $command {*}$args
- }
- }
-
-
- #review.
- # defaults to calling namespace - but can be arbitrary string
- proc rename_command {args} {
- #todo: consider -forcebase 1 or similar to allow this rename to point to bottom of stack (original command) bypassing existing renames
- # - need to consider that upon removing, that any remaining rename that was higher on the stack should not also be diverted to the base - but rather to the next lower in the stack
- #
- if {[lindex $args 0] eq "-renamer"} {
- set renamer [lindex $args 1]
- set arglist [lrange $args 2 end]
- } else {
- set renamer ""
- set arglist $args
- }
- if {[llength $arglist] != 3} {
- error "commandstack::rename_command usage: rename_command ?-renamer ? command procargs procbody"
- }
- lassign $arglist command procargs procbody
-
- set command [uplevel 1 [list namespace which $command]]
- set mungedcommand [string map {:: _ns_} $command]
- set mungedrenamer [string map {:: _ns_} $renamer]
- variable all_stacks
- variable known_renamers
- variable renamer_command_tokens ;#monotonically increasing int per :: representing number of renames ever done.
- if {$renamer eq ""} {
- set renamer [uplevel 1 [list namespace current]]
- }
- if {$renamer ni $known_renamers} {
- lappend known_renamers $renamer
- dict set renamer_command_tokens [list $renamer $command] 0
- }
-
- #TODO - reduce emissions to stderr - flag for debug?
-
- #e.g packageTrace and packageSuppress packages use this convention.
- set nextinfo [uplevel 1 [list\
- apply {{command renamer procbody} {
- #todo - munge dash so we can make names in renamed_commands separable
- # {- _dash_} ?
- set mungedcommand [string map {:: _ns_} $command]
- set mungedrenamer [string map {:: _ns_} $renamer]
- set tokenid [lindex [dict incr renamer_command_tokens [list $renamer $command]] 1]
- set next_target ::commandstack::renamed_commands::${mungedcommand}-original-$mungedrenamer-$tokenid ;#default is to assume we are the only one playing around with it, but we'll check for known associates too.
- set do_rename 0
- if {[llength [info procs $command]] || [llength [info commands $next_target]]} {
- #$command is not the standard builtin - something has replaced it, could be ourself.
- set next_implementor [::commandstack::util::get_IMPLEMENTOR $command]
- set munged_next_implementor [string map {:: _ns_} $next_implementor]
- #if undetermined/unspecified it could be the latest renamer on the stack - but we can't know for sure something else didn't rename it.
- if {[dict exists $::commandstack::all_stacks $command]} {
- set comstacks [dict get $::commandstack::all_stacks $command]
- } else {
- set comstacks [list]
- }
- set this_renamer_previous_entries [lsearch -all -index 3 $comstacks $renamer] ;#index 3 is value for second dict entry - (value for key 'renamer')
- if {[llength $this_renamer_previous_entries]} {
- if {$next_implementor eq $renamer} {
- #previous renamer was us. Rather than assume our job is done.. compare the implementations
- #don't rename if immediate predecessor is same code.
- #set topstack [lindex $comstacks end]
- #set next_impl [dict get $topstack implementation]
- set current_body [info body $command]
- lassign [commandstack::lib::split_body $current_body] _ current_code
- set current_code [string trim $current_code]
- set new_code [string trim $procbody]
- if {$current_code eq $new_code} {
- puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command with same procbody - Aborting rename."
- puts stderr [::commandstack::show_stack $command]
- } else {
- puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command - but appears to be with new code - proceeding."
- puts stdout "----------"
- puts stdout "$current_code"
- puts stdout "----------"
- puts stdout "$new_code"
- puts stdout "----------"
- set next_target ::commandstack::renamed_commands::${mungedcommand}-${munged_next_implementor}-$mungedrenamer-$tokenid
- set do_rename 1
- }
- } else {
- puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command, but is not immediate predecessor - proceeding anyway... (untested)"
- puts stderr
- set next_target ::commandstack::renamed_commands::${mungedcommand}-${munged_next_implementor}-$mungedrenamer-$tokenid
- set do_rename 1
- }
- } elseif {$next_implementor in $::commandstack::known_renamers} {
- set next_target ::commandstack::renamed_commands::${mungedcommand}-${munged_next_implementor}-$mungedrenamer-$tokenid
- set do_rename 1
- } elseif {$next_implementor in {builtin}} {
- #native/builtin could still have been renamed
- set next_target ::commandstack::renamed_commands::${mungedcommand}_${munged_next_implementor}-$mungedrenamer-$tokenid
- set do_rename 1
- } elseif {$next_implementor in {unspecified undetermined}} {
- #could be a standard tcl proc, or from application or package
- set next_target ::commandstack::renamed_commands::${mungedcommand}_${munged_next_implementor}-$mungedrenamer-$tokenid
- set do_rename 1
- } else {
- puts stderr "(commandstack::rename_command) Warning - pkg:'$next_implementor' has renamed the '$command' command. Attempting to cooperate. (untested)"
- set next_target ::commandstack::renamed_commands::${mungedcommand}_${munged_next_implementor}-$mungedrenamer-$tokenid
- set do_rename 1
- }
- } else {
- #_originalcommand_
- #assume builtin/original
- set next_implementor original
- #rename $command $next_target
- set do_rename 1
- }
- #There are of course other ways in which $command may have been renamed - but we can't detect.
- set token [list $command $renamer $tokenid]
- return [dict create next_target $next_target next_implementor $next_implementor token $token do_rename $do_rename]
- } } $command $renamer $procbody]
- ]
-
-
- variable debug
- if {$debug} {
- if {[dict exists $all_stacks $command]} {
- set stack [dict get $all_stacks $command]
- puts stderr "(commandstack::rename_command) Subsequent rename of command '$command'. (previous renames: [llength $stack]). Renaming to [dict get $nextinfo next_target]"
- } else {
- #assume this is the original
- puts stderr "(commandstack::rename_command) 1st detected rename of command '$command'. Renaming to [dict get $nextinfo next_target]"
- }
- }
-
- #token is always first dict entry. (Value needs to be searched with lsearch -index 1 )
- #renamer is always second dict entry (Value needs to be searched with lsearch -index 3)
- set new_record [dict create\
- token [dict get $nextinfo token]\
- renamer $renamer\
- next_implementor [dict get $nextinfo next_implementor]\
- next_getter [list ::commandstack::get_next_command {*}[dict get $nextinfo token]]\
- implementation [dict get $nextinfo next_target]\
- ]
- if {![dict get $nextinfo do_rename]} {
- #review
- puts stderr "no rename performed"
- return [dict create implementation ""]
- }
- catch {rename ::commandstack::temp::testproc ""}
- set nextinit [string map [list %command% $command %renamer% $renamer %next_getter% [dict get $new_record next_getter] %original_implementation% [dict get $new_record implementation]] {
- #IMPLEMENTOR_%renamer%! (mechanism: 'commandstack::rename_command -renamer %renamer% %command% )
- set COMMANDSTACKNEXT_ORIGINAL %original_implementation% ;#informational/debug for overriding proc.
- set COMMANDSTACKNEXT [%next_getter%]
- ##
- }]
- set final_procbody "$nextinit$procbody"
- #build the proc at a temp location so that if it raises an error we don't adjust the stack or replace the original command
- #(e.g due to invalid argument specifiers)
- proc ::commandstack::temp::testproc $procargs $final_procbody
- uplevel 1 [list rename $command [dict get $nextinfo next_target]]
- uplevel 1 [list rename ::commandstack::temp::testproc $command]
- dict lappend all_stacks $command $new_record
-
-
- return $new_record
- }
-
- #todo - concept of 'pop' for renamer. Remove topmost entry specific to the renamer
- #todo - removal by token to allow renamer to have multiple entries for one command but to remove one that is not the topmost
- #todo - removal of all entries pertaining to a particular renamer
- #todo - allow restore to bottom-most implementation (original) - regardless of what renamers have cooperated in the stack?
-
- #remove by token, or by commandname if called from same context as original rename_command
- #If only a commandname is supplied, and there were multiple renames from the same context (same -renamer) only the topmost is removed.
- #A call to remove_rename with no token or renamer, and from a namespace context which didn't perform a rename will not remove anything.
- #similarly a nonexistant token or renamer will not remove anything and will just return the current stack
- proc remove_rename {token_or_command} {
- if {[llength $token_or_command] == 3} {
- #is token
- lassign $token_or_command command renamer tokenid
- } elseif {[llength $token_or_command] == 2} {
- #command and renamer only supplied
- lassign $token_or_command command renamer
- set tokenid ""
- } elseif {[llength $token_or_command] == 1} {
- #is command name only
- set command $token_or_command
- set renamer [uplevel 1 [list namespace current]]
- set tokenid ""
- }
- set command [uplevel 1 [list namespace which $command]]
- variable all_stacks
- variable known_renamers
- if {$renamer ni $known_renamers} {
- error "(commandstack::remove_rename) ERROR: renamer $renamer not in list of known_renamers '$known_renamers' for command '$command'. Ensure remove_rename called from same context as rename_command was, or explicitly supply exact token or {}"
- }
- if {[dict exists $all_stacks $command]} {
- set stack [dict get $all_stacks $command]
- if {$tokenid ne ""} {
- #token_or_command is a token as returned within the rename_command result dictionary
- #search first dict value
- set doomed_posn [lsearch -index 1 $stack $token_or_command]
- } else {
- #search second dict value
- set matches [lsearch -all -index 3 $stack $renamer]
- set doomed_posn [lindex $matches end] ;#we don't have a full token - pop last entry for this renamer
- }
- if {$doomed_posn ne "" && $doomed_posn > -1} {
- set doomed_record [lindex $stack $doomed_posn]
- if {[llength $stack] == ($doomed_posn + 1)} {
- #last on stack - put the implemenation from the doomed_record back as the actual command
- uplevel #0 [list rename $command ""]
- uplevel #0 [list rename [dict get $doomed_record implementation] $command]
- } elseif {[llength $stack] > ($doomed_posn + 1)} {
- #there is at least one more record on the stack - rewrite it to point where the doomed_record pointed
- set rewrite_posn [expr {$doomed_posn + 1}]
- set rewrite_record [lindex $stack $rewrite_posn]
-
- if {[dict get $rewrite_record next_implementor] ne $renamer} {
- puts stderr "(commandstack::remove_rename) WARNING: next record on the commandstack didn't record '$renamer' as the next_implementor - not deleting implementation [dict get $rewrite_record implementation]"
- } else {
- uplevel #0 [list rename [dict get $rewrite_record implementation] ""]
- }
- dict set rewrite_record next_implementor [dict get $doomed_record next_implementor]
- #don't update next_getter - it always refers to self
- dict set rewrite_record implementation [dict get $doomed_record implementation]
- lset stack $rewrite_posn $rewrite_record
- dict set all_stacks $command $stack
- }
- set stack [lreplace $stack $doomed_posn $doomed_posn]
- dict set all_stacks $command $stack
-
- }
- return $stack
- }
- return [list]
- }
-
- proc show_stack {{commandname_glob *}} {
- variable all_stacks
- if {![regexp {[?*]} $commandname_glob]} {
- #if caller is attempting exact match - use the calling context to resolve in case they didn't supply namespace
- set commandname_glob [uplevel 1 [list namespace which $commandname_glob]]
- }
- if {[package provide punk::lib] ne "" && [package provide punk] ne ""} {
- #punk pipeline also needed for patterns
- return [punk::lib::pdict -channel none all_stacks $commandname_glob/@*/@*.@*]
- } else {
- set result ""
- set matchedkeys [dict keys $all_stacks $commandname_glob]
- #don't try to calculate widest on empty list
- if {[llength $matchedkeys]} {
- set widest [tcl::mathfunc::max {*}[lmap v $matchedkeys {tcl::string::length $v}]]
- set indent [string repeat " " [expr {$widest + 3}]]
- set indent2 "${indent} " ;#8 spaces for " i = " where i is 4 wide
- set padkey [string repeat " " 20]
- foreach k $matchedkeys {
- append result "$k = "
- set i 0
- foreach stackmember [dict get $all_stacks $k] {
- if {$i > 0} {
- append result "\n$indent"
- }
- append result [string range "$i " 0 4] " = "
- set j 0
- dict for {k v} $stackmember {
- if {$j > 0} {
- append result "\n$indent2"
- }
- set displaykey [string range "$k$padkey" 0 20]
- append result "$displaykey = $v"
- incr j
- }
- incr i
- }
- append result \n
- }
- }
- return $result
- }
- }
-
- #review
- #document when this is to be called. Wiping stacks without undoing renames seems odd.
- proc Delete_stack {command} {
- variable all_stacks
- if {[dict exists $all_stacks $command]} {
- dict unset all_stacks $command
- return 1
- } else {
- return 1
- }
- }
-
- #can be used to temporarily put a stack aside - should manually rename back when done.
- #review - document how/when to use. example? intention?
- proc Rename_stack {oldname newname} {
- variable all_stacks
- if {[dict exists $all_stacks $oldname]} {
- if {[dict exists $all_stacks $newname]} {
- error "(commandstack::rename_stack) cannot rename $oldname to $newname - $newname already exists in stack"
- } else {
- #set stackval [dict get $all_stacks $oldname]
- #dict unset all_stacks $oldname
- #dict set all_stacks $newname $stackval
- dict set all_stacks $newname [lindex [list [dict get $all_stacks $oldname] [dict unset all_stacks $oldname]] 0]
- }
- }
- }
-}
-
-
-
-
-
-
-
-
-namespace eval commandstack::lib {
- proc splitx {str {regexp {[\t \r\n]+}}} {
- #snarfed from tcllib textutil::splitx to avoid the dependency
- # Bugfix 476988
- if {[string length $str] == 0} {
- return {}
- }
- if {[string length $regexp] == 0} {
- return [::split $str ""]
- }
- if {[regexp $regexp {}]} {
- return -code error "splitting on regexp \"$regexp\" would cause infinite loop"
- }
-
- set list {}
- set start 0
- while {[regexp -start $start -indices -- $regexp $str match submatch]} {
- foreach {subStart subEnd} $submatch break
- foreach {matchStart matchEnd} $match break
- incr matchStart -1
- incr matchEnd
- lappend list [string range $str $start $matchStart]
- if {$subStart >= $start} {
- lappend list [string range $str $subStart $subEnd]
- }
- set start $matchEnd
- }
- lappend list [string range $str $start end]
- return $list
- }
- proc split_body {procbody} {
- set marker "##"
- set header ""
- set code ""
- set found_marker 0
- foreach ln [split $procbody \n] {
- if {!$found_marker} {
- if {[string trim $ln] eq $marker} {
- set found_marker 1
- } else {
- append header $ln \n
- }
- } else {
- append code $ln \n
- }
- }
- if {$found_marker} {
- return [list $header $code]
- } else {
- return [list "" $procbody]
- }
- }
-}
-
-package provide commandstack [namespace eval commandstack {
- set version 0.3
-}]
-
-
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/debug-1.0.6.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/debug-1.0.6.tm
deleted file mode 100644
index c2ee57be..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/debug-1.0.6.tm
+++ /dev/null
@@ -1,306 +0,0 @@
-# Debug - a debug narrative logger.
-# -- Colin McCormack / originally Wub server utilities
-#
-# Debugging areas of interest are represented by 'tokens' which have
-# independantly settable levels of interest (an integer, higher is more detailed)
-#
-# Debug narrative is provided as a tcl script whose value is [subst]ed in the
-# caller's scope if and only if the current level of interest matches or exceeds
-# the Debug call's level of detail. This is useful, as one can place arbitrarily
-# complex narrative in code without unnecessarily evaluating it.
-#
-# TODO: potentially different streams for different areas of interest.
-# (currently only stderr is used. there is some complexity in efficient
-# cross-threaded streams.)
-
-# # ## ### ##### ######## ############# #####################
-## Requisites
-
-package require Tcl 8.5-
-
-namespace eval ::debug {
- namespace export -clear \
- define on off prefix suffix header trailer \
- names 2array level setting parray pdict \
- nl tab hexl
- namespace ensemble create -subcommands {}
-}
-
-# # ## ### ##### ######## ############# #####################
-## API & Implementation
-
-proc ::debug::noop {args} {}
-
-proc ::debug::debug {tag message {level 1}} {
- variable detail
- if {$detail($tag) < $level} {
- #puts stderr "$tag @@@ $detail($tag) >= $level"
- return
- }
-
- variable prefix
- variable suffix
- variable header
- variable trailer
- variable fds
-
- if {[info exists fds($tag)]} {
- set fd $fds($tag)
- } else {
- set fd stderr
- }
-
- # Assemble the shown text from the user message and the various
- # prefixes and suffices (global + per-tag).
-
- set themessage ""
- if {[info exists prefix(::)]} { append themessage $prefix(::) }
- if {[info exists prefix($tag)]} { append themessage $prefix($tag) }
- append themessage $message
- if {[info exists suffix($tag)]} { append themessage $suffix($tag) }
- if {[info exists suffix(::)]} { append themessage $suffix(::) }
-
- # Resolve variables references and command invokations embedded
- # into the message with plain text.
- set code [catch {
- set smessage [uplevel 1 [list ::subst -nobackslashes $themessage]]
- set sheader [uplevel 1 [list ::subst -nobackslashes $header]]
- set strailer [uplevel 1 [list ::subst -nobackslashes $trailer]]
- } __ eo]
-
- # And dump an internal error if that resolution failed.
- if {$code} {
- if {[catch {
- set caller [info level -1]
- }]} { set caller GLOBAL }
- if {[string length $caller] >= 1000} {
- set caller "[string range $caller 0 200]...[string range $caller end-200 end]"
- }
- foreach line [split $caller \n] {
- puts -nonewline $fd "@@(DebugError from $tag ($eo): $line)"
- }
- return
- }
-
- # From here we have a good message to show. We only shorten it a
- # bit if its a bit excessive in size.
-
- if {[string length $smessage] > 4096} {
- set head [string range $smessage 0 2048]
- set tail [string range $smessage end-2048 end]
- set smessage "${head}...(truncated)...$tail"
- }
-
- foreach line [split $smessage \n] {
- puts $fd "$sheader$tag | $line$strailer"
- }
- return
-}
-
-# names - return names of debug tags
-proc ::debug::names {} {
- variable detail
- return [lsort [array names detail]]
-}
-
-proc ::debug::2array {} {
- variable detail
- set result {}
- foreach n [lsort [array names detail]] {
- if {[interp alias {} debug.$n] ne "::debug::noop"} {
- lappend result $n $detail($n)
- } else {
- lappend result $n -$detail($n)
- }
- }
- return $result
-}
-
-# level - set level and fd for tag
-proc ::debug::level {tag {level ""} {fd {}}} {
- variable detail
- # TODO: Force level >=0.
- if {$level ne ""} {
- set detail($tag) $level
- }
-
- if {![info exists detail($tag)]} {
- set detail($tag) 1
- }
-
- variable fds
- if {$fd ne {}} {
- set fds($tag) $fd
- }
-
- return $detail($tag)
-}
-
-proc ::debug::header {text} { variable header $text }
-proc ::debug::trailer {text} { variable trailer $text }
-
-proc ::debug::define {tag} {
- if {[interp alias {} debug.$tag] ne {}} return
- off $tag
- return
-}
-
-# Set a prefix/suffix to use for tag.
-# The global (tag-independent) prefix/suffix is adressed through tag '::'.
-# This works because colon (:) is an illegal character for user-specified tags.
-
-proc ::debug::prefix {tag {theprefix {}}} {
- variable prefix
- set prefix($tag) $theprefix
-
- if {[interp alias {} debug.$tag] ne {}} return
- off $tag
- return
-}
-
-proc ::debug::suffix {tag {theprefix {}}} {
- variable suffix
- set suffix($tag) $theprefix
-
- if {[interp alias {} debug.$tag] ne {}} return
- off $tag
- return
-}
-
-# turn on debugging for tag
-proc ::debug::on {tag {level ""} {fd {}}} {
- variable active
- set active($tag) 1
- level $tag $level $fd
- interp alias {} debug.$tag {} ::debug::debug $tag
- return
-}
-
-# turn off debugging for tag
-proc ::debug::off {tag {level ""} {fd {}}} {
- variable active
- set active($tag) 1
- level $tag $level $fd
- interp alias {} debug.$tag {} ::debug::noop
- return
-}
-
-proc ::debug::setting {args} {
- if {[llength $args] == 1} {
- set args [lindex $args 0]
- }
- set fd stderr
- if {[llength $args] % 2} {
- set fd [lindex $args end]
- set args [lrange $args 0 end-1]
- }
- foreach {tag level} $args {
- if {$level > 0} {
- level $tag $level $fd
- interp alias {} debug.$tag {} ::debug::debug $tag
- } else {
- level $tag [expr {-$level}] $fd
- interp alias {} debug.$tag {} ::debug::noop
- }
- }
- return
-}
-
-# # ## ### ##### ######## ############# #####################
-## Convenience commands.
-# Format arrays and dicts as multi-line message.
-# Insert newlines and tabs.
-
-proc ::debug::nl {} { return \n }
-proc ::debug::tab {} { return \t }
-
-proc ::debug::parray {a {pattern *}} {
- upvar 1 $a array
- if {![array exists array]} {
- error "\"$a\" isn't an array"
- }
- pdict [array get array] $pattern
-}
-
-proc ::debug::pdict {dict {pattern *}} {
- set maxl 0
- set names [lsort -dict [dict keys $dict $pattern]]
- foreach name $names {
- if {[string length $name] > $maxl} {
- set maxl [string length $name]
- }
- }
- set maxl [expr {$maxl + 2}]
- set lines {}
- foreach name $names {
- set nameString [format (%s) $name]
- lappend lines [format "%-*s = %s" \
- $maxl $nameString \
- [dict get $dict $name]]
- }
- return [join $lines \n]
-}
-
-proc ::debug::hexl {data {prefix {}}} {
- set r {}
-
- # Convert the data to hex and to characters.
- binary scan $data H*@0a* hexa asciia
-
- # Replace non-printing characters in the data with dots.
- regsub -all -- {[^[:graph:] ]} $asciia {.} asciia
-
- # Pad with spaces to a full multiple of 32/16.
- set n [expr {[string length $hexa] % 32}]
- if {$n < 32} { append hexa [string repeat { } [expr {32-$n}]] }
- #puts "pad H [expr {32-$n}]"
-
- set n [expr {[string length $asciia] % 32}]
- if {$n < 16} { append asciia [string repeat { } [expr {16-$n}]] }
- #puts "pad A [expr {32-$n}]"
-
- # Reassemble formatted, in groups of 16 bytes/characters.
- # The hex part is handled in groups of 32 nibbles.
- set addr 0
- while {[string length $hexa]} {
- # Get front group of 16 bytes each.
- set hex [string range $hexa 0 31]
- set ascii [string range $asciia 0 15]
- # Prep for next iteration
- set hexa [string range $hexa 32 end]
- set asciia [string range $asciia 16 end]
-
- # Convert the hex to pairs of hex digits
- regsub -all -- {..} $hex {& } hex
-
- # Add the hex and latin-1 data to the result buffer
- append r $prefix [format %04x $addr] { | } $hex { |} $ascii |\n
- incr addr 16
- }
-
- # And done
- return $r
-}
-
-# # ## ### ##### ######## ############# #####################
-
-namespace eval debug {
- variable detail ; # map: TAG -> level of interest
- variable prefix ; # map: TAG -> message prefix to use
- variable suffix ; # map: TAG -> message suffix to use
- variable fds ; # map: TAG -> handle of open channel to log to.
- variable header {} ; # per-line heading, subst'ed
- variable trailer {} ; # per-line ending, subst'ed
-
- # Notes:
- # - The tag '::' is reserved. "prefix" and "suffix" use it to store
- # the global message prefix / suffix.
- # - prefix and suffix are applied per message.
- # - header and trailer are per line. And should not generate multiple lines!
-}
-
-# # ## ### ##### ######## ############# #####################
-## Ready
-
-package provide debug 1.0.6
-return
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/dictn-0.1.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/dictn-0.1.2.tm
deleted file mode 100644
index 2ed2b1ef..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/dictn-0.1.2.tm
+++ /dev/null
@@ -1,366 +0,0 @@
-# -*- tcl -*-
-# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt
-#
-# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
-# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# (C) 2023
-#
-# @@ Meta Begin
-# Application dictn 0.1.2
-# Meta platform tcl
-# Meta license
-# @@ Meta End
-
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Requirements
-##e.g package require frobz
-
-
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-namespace eval dictn {
- namespace export {[a-z]*}
- namespace ensemble create
-}
-
-
-## ::dictn::append
-#This can of course 'ruin' a nested dict if applied to the wrong element
-# - i.e using the string op 'append' on an element that is itself a nested dict is analogous to the standard Tcl:
-# %set list {a b {c d}}
-# %append list x
-# a b {c d}x
-# IOW - don't do that unless you really know that's what you want.
-#
-proc ::dictn::append {dictvar path {value {}}} {
- if {[llength $path] == 1} {
- uplevel 1 [list dict append $dictvar $path $value]
- } else {
- upvar 1 $dictvar dvar
-
- ::set str [dict get $dvar {*}$path]
- append str $val
- dict set dvar {*}$path $str
- }
-}
-
-proc ::dictn::create {args} {
- ::set data {}
- foreach {path val} $args {
- dict set data {*}$path $val
- }
- return $data
-}
-
-proc ::dictn::exists {dictval path} {
- return [dict exists $dictval {*}$path]
-}
-
-proc ::dictn::filter {dictval path filterType args} {
- ::set sub [dict get $dictval {*}$path]
- dict filter $sub $filterType {*}$args
-}
-
-proc ::dictn::for {keyvalvars dictval path body} {
- ::set sub [dict get $dictval {*}$path]
- dict for $keyvalvars $sub $body
-}
-
-proc ::dictn::get {dictval {path {}}} {
- return [dict get $dictval {*}$path]
-}
-
-
-if {[info commands ::tcl::dict::getdef] ne ""} {
- #tcl 9+
- proc ::dictn::getdef {dictval path default} {
- return [dict getdef $dictval {*}$path $default]
- }
-
- proc ::dictn::getwithdefault {dictval path default} {
- return [dict getdef $dictval {*}$path $default]
- }
-
- proc ::dictn::incr {dictvar path {increment {}} } {
- if {$increment eq ""} {
- ::set increment 1
- }
- if {[llength $path] == 1} {
- uplevel 1 [list dict incr $dictvar $path $increment]
- } else {
- upvar 1 $dictvar dvar
- if {![::info exists dvar]} {
- dict set dvar {*}$path $increment
- } else {
- ::set newval [expr {[dict getdef $dvar {*}$path 0] + $increment}]
- dict set dvar {*}$path $newval
- }
- return $dvar
- }
- }
-} else {
- #tcl < 9
- proc ::dictn::getdef {dictval path default} {
- if {[tcl::dict::exists $dictval {*}$path]} {
- return [tcl::dict::get $dictval {*}$path]
- } else {
- return $default
- }
- }
- proc ::dictn::getwithdefault {dictval path default} {
- if {[tcl::dict::exists $dictval {*}$path]} {
- return [tcl::dict::get $dictval {*}$path]
- } else {
- return $default
- }
- }
- proc ::dictn::incr {dictvar path {increment {}} } {
- if {$increment eq ""} {
- ::set increment 1
- }
- if {[llength $path] == 1} {
- uplevel 1 [list dict incr $dictvar $path $increment]
- } else {
- upvar 1 $dictvar dvar
- if {![::info exists dvar]} {
- dict set dvar {*}$path $increment
- } else {
- if {![dict exists $dvar {*}$path]} {
- ::set val 0
- } else {
- ::set val [dict get $dvar {*}$path]
- }
- ::set newval [expr {$val + $increment}]
- dict set dvar {*}$path $newval
- }
- return $dvar
- }
- }
-}
-
-proc ::dictn::info {dictval {path {}}} {
- if {![string length $path]} {
- return [dict info $dictval]
- } else {
- ::set sub [dict get $dictval {*}$path]
- return [dict info $sub]
- }
-}
-
-proc ::dictn::keys {dictval {path {}} {glob {}}} {
- ::set sub [dict get $dictval {*}$path]
- if {[string length $glob]} {
- return [dict keys $sub $glob]
- } else {
- return [dict keys $sub]
- }
-}
-
-proc ::dictn::lappend {dictvar path args} {
- if {[llength $path] == 1} {
- uplevel 1 [list dict lappend $dictvar $path {*}$args]
- } else {
- upvar 1 $dictvar dvar
-
- ::set list [dict get $dvar {*}$path]
- ::lappend list {*}$args
- dict set dvar {*}$path $list
- }
-}
-
-proc ::dictn::merge {args} {
- error "nested merge not yet supported"
-}
-
-#dictn remove dictionaryValue ?path ...?
-proc ::dictn::remove {dictval args} {
- ::set basic [list] ;#buffer basic (1element path) removals to do in a single call.
-
- foreach path $args {
- if {[llength $path] == 1} {
- ::lappend basic $path
- } else {
- #extract,modify,replace
- ::set subpath [lrange $path 0 end-1]
-
- ::set sub [dict get $dictval {*}$subpath]
- ::set sub [dict remove $sub [lindex $path end]]
-
- dict set dictval {*}$subpath $sub
- }
- }
-
- if {[llength $basic]} {
- return [dict remove $dictval {*}$basic]
- } else {
- return $dictval
- }
-}
-
-
-proc ::dictn::replace {dictval args} {
- ::set basic [list] ;#buffer basic (1element path) replacements to do in a single call.
-
- foreach {path val} $args {
- if {[llength $path] == 1} {
- ::lappend basic $path $val
- } else {
- #extract,modify,replace
- ::set subpath [lrange $path 0 end-1]
-
- ::set sub [dict get $dictval {*}$subpath]
- ::set sub [dict replace $sub [lindex $path end] $val]
-
- dict set dictval {*}$subpath $sub
- }
- }
-
-
- if {[llength $basic]} {
- return [dict replace $dictval {*}$basic]
- } else {
- return $dictval
- }
-}
-
-
-proc ::dictn::set {dictvar path newval} {
- upvar 1 $dictvar dvar
- return [dict set dvar {*}$path $newval]
-}
-
-proc ::dictn::size {dictval {path {}}} {
- return [dict size [dict get $dictval {*}$path]]
-}
-
-proc ::dictn::unset {dictvar path} {
- upvar 1 $dictvar dvar
- return [dict unset dvar {*}$path
-}
-
-proc ::dictn::update {dictvar args} {
- ::set body [lindex $args end]
- ::set maplist [lrange $args 0 end-1]
-
- upvar 1 $dictvar dvar
- foreach {path var} $maplist {
- if {[dict exists $dvar {*}$path]} {
- uplevel 1 [list set $var [dict get $dvar $path]]
- }
- }
-
- catch {uplevel 1 $body} result
-
- foreach {path var} $maplist {
- if {[dict exists $dvar {*}$path]} {
- upvar 1 $var $var
- if {![::info exists $var]} {
- uplevel 1 [list dict unset $dictvar {*}$path]
- } else {
- uplevel 1 [list dict set $dictvar {*}$path [::set $var]]
- }
- }
- }
- return $result
-}
-
-#an experiment.
-proc ::dictn::Applyupdate {dictvar args} {
- ::set body [lindex $args end]
- ::set maplist [lrange $args 0 end-1]
-
- upvar 1 $dictvar dvar
-
- ::set headscript ""
- ::set i 0
- foreach {path var} $maplist {
- if {[dict exists $dvar {*}$path]} {
- #uplevel 1 [list set $var [dict get $dvar $path]]
- ::lappend arglist $var
- ::lappend vallist [dict get $dvar {*}$path]
- ::append headscript [string map [list %i% $i %v% $var] {upvar 1 %v% %v%; set %v% [lindex $args %i%]} ]
- ::append headscript \n
- ::incr i
- }
- }
-
- ::set body $headscript\r\n$body
-
- puts stderr "BODY: $body"
-
- #set result [apply [list args $body] {*}$vallist]
- catch {apply [list args $body] {*}$vallist} result
-
- foreach {path var} $maplist {
- if {[dict exists $dvar {*}$path] && [::info exists $var]} {
- dict set dvar {*}$path [::set $var]
- }
- }
- return $result
-}
-
-proc ::dictn::values {dictval {path {}} {glob {}}} {
- ::set sub [dict get $dictval {*}$path]
- if {[string length $glob]} {
- return [dict values $sub $glob]
- } else {
- return [dict values $sub]
- }
-}
-
-# Standard form:
-#'dictn with dictVariable path body'
-#
-# Extended form:
-#'dictn with dictVariable path arrayVariable body'
-#
-proc ::dictn::with {dictvar path args} {
- if {[llength $args] == 1} {
- ::set body [lindex $args 0]
- return [uplevel 1 [list dict with $dictvar {*}$path $body]]
- } else {
- upvar 1 $dictvar dvar
- ::lassign $args arrayname body
-
- upvar 1 $arrayname arr
- array set arr [dict get $dvar {*}$path]
- ::set prevkeys [array names arr]
-
- catch {uplevel 1 $body} result
-
-
- foreach k $prevkeys {
- if {![::info exists arr($k)]} {
- dict unset $dvar {*}$path $k
- }
- }
- foreach k [array names arr] {
- dict set $dvar {*}$path $k $arr($k)
- }
-
- return $result
- }
-}
-
-
-
-
-
-
-
-
-
-
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Ready
-package provide dictn [namespace eval dictn {
- variable version
- ::set version 0.1.2
-}]
-return
\ No newline at end of file
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/dictutils-0.2.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/dictutils-0.2.1.tm
deleted file mode 100644
index 12ca495b..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/dictutils-0.2.1.tm
+++ /dev/null
@@ -1,145 +0,0 @@
-# dictutils.tcl --
- #
- # Various dictionary utilities.
- #
- # Copyright (c) 2007 Neil Madden (nem@cs.nott.ac.uk).
- #
- # License: http://www.cs.nott.ac.uk/~nem/license.terms (Tcl-style).
- #
-
- #2023 0.2.1 - changed "package require Tcl 8.6" to "package require Tcl 8.6-"
-
- package require Tcl 8.6-
- package provide dictutils 0.2.1
-
- namespace eval dictutils {
- namespace export equal apply capture witharray nlappend
- namespace ensemble create
-
- # dictutils witharray dictVar arrayVar script --
- #
- # Unpacks the elements of the dictionary in dictVar into the array
- # variable arrayVar and then evaluates the script. If the script
- # completes with an ok, return or continue status, then the result is copied
- # back into the dictionary variable, otherwise it is discarded. A
- # [break] can be used to explicitly abort the transaction.
- #
- proc witharray {dictVar arrayVar script} {
- upvar 1 $dictVar dict $arrayVar array
- array set array $dict
- try { uplevel 1 $script
- } on break {} { # Discard the result
- } on continue result - on ok result {
- set dict [array get array] ;# commit changes
- return $result
- } on return {result opts} {
- set dict [array get array] ;# commit changes
- dict incr opts -level ;# remove this proc from level
- return -options $opts $result
- }
- # All other cases will discard the changes and propagage
- }
-
- # dictutils equal equalp d1 d2 --
- #
- # Compare two dictionaries for equality. Two dictionaries are equal
- # if they (a) have the same keys, (b) the corresponding values for
- # each key in the two dictionaries are equal when compared using the
- # equality predicate, equalp (passed as an argument). The equality
- # predicate is invoked with the key and the two values from each
- # dictionary as arguments.
- #
- proc equal {equalp d1 d2} {
- if {[dict size $d1] != [dict size $d2]} { return 0 }
- dict for {k v} $d1 {
- if {![dict exists $d2 $k]} { return 0 }
- if {![invoke $equalp $k $v [dict get $d2 $k]]} { return 0 }
- }
- return 1
- }
-
- # apply dictVar lambdaExpr ?arg1 arg2 ...? --
- #
- # A combination of *dict with* and *apply*, this procedure creates a
- # new procedure scope populated with the values in the dictionary
- # variable. It then applies the lambdaTerm (anonymous procedure) in
- # this new scope. If the procedure completes normally, then any
- # changes made to variables in the dictionary are reflected back to
- # the dictionary variable, otherwise they are ignored. This provides
- # a transaction-style semantics whereby atomic updates to a
- # dictionary can be performed. This procedure can also be useful for
- # implementing a variety of control constructs, such as mutable
- # closures.
- #
- proc apply {dictVar lambdaExpr args} {
- upvar 1 $dictVar dict
- set env $dict ;# copy
- lassign $lambdaExpr params body ns
- if {$ns eq ""} { set ns "::" }
- set body [format {
- upvar 1 env __env__
- dict with __env__ %s
- } [list $body]]
- set lambdaExpr [list $params $body $ns]
- set rc [catch { ::apply $lambdaExpr {*}$args } ret opts]
- if {$rc == 0} {
- # Copy back any updates
- set dict $env
- }
- return -options $opts $ret
- }
-
- # capture ?level? ?exclude? ?include? --
- #
- # Captures a snapshot of the current (scalar) variable bindings at
- # $level on the stack into a dictionary environment. This dictionary
- # can later be used with *dictutils apply* to partially restore the
- # scope, creating a first approximation of closures. The *level*
- # argument should be of the forms accepted by *uplevel* and
- # designates which level to capture. It defaults to 1 as in uplevel.
- # The *exclude* argument specifies an optional list of literal
- # variable names to avoid when performing the capture. No variables
- # matching any item in this list will be captured. The *include*
- # argument can be used to specify a list of glob patterns of
- # variables to capture. Only variables matching one of these
- # patterns are captured. The default is a single pattern "*", for
- # capturing all visible variables (as determined by *info vars*).
- #
- proc capture {{level 1} {exclude {}} {include {*}}} {
- if {[string is integer $level]} { incr level }
- set env [dict create]
- foreach pattern $include {
- foreach name [uplevel $level [list info vars $pattern]] {
- if {[lsearch -exact -index 0 $exclude $name] >= 0} { continue }
- upvar $level $name value
- catch { dict set env $name $value } ;# no arrays
- }
- }
- return $env
- }
-
- # nlappend dictVar keyList ?value ...?
- #
- # Append zero or more elements to the list value stored in the given
- # dictionary at the path of keys specified in $keyList. If $keyList
- # specifies a non-existent path of keys, nlappend will behave as if
- # the path mapped to an empty list.
- #
- proc nlappend {dictvar keylist args} {
- upvar 1 $dictvar dict
- if {[info exists dict] && [dict exists $dict {*}$keylist]} {
- set list [dict get $dict {*}$keylist]
- }
- lappend list {*}$args
- dict set dict {*}$keylist $list
- }
-
- # invoke cmd args... --
- #
- # Helper procedure to invoke a callback command with arguments at
- # the global scope. The helper ensures that proper quotation is
- # used. The command is expected to be a list, e.g. {string equal}.
- #
- proc invoke {cmd args} { uplevel #0 $cmd $args }
-
- }
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/fauxlink-0.1.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/fauxlink-0.1.1.tm
deleted file mode 100644
index 970e47da..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/fauxlink-0.1.1.tm
+++ /dev/null
@@ -1,568 +0,0 @@
-# -*- tcl -*-
-# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt
-#
-# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
-# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# (C) 2024
-#
-# @@ Meta Begin
-# Application fauxlink 0.1.1
-# Meta platform tcl
-# Meta license MIT
-# @@ Meta End
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# doctools header
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-#*** !doctools
-#[manpage_begin fauxlink_module_fauxlink 0 0.1.1]
-#[copyright "2024"]
-#[titledesc {faux link application shortcuts}] [comment {-- Name section and table of contents description --}]
-#[moddesc {.fauxlink .fxlnk}] [comment {-- Description at end of page heading --}]
-#[require fauxlink]
-#[keywords symlink faux fake shortcut toml]
-#[description]
-#[para] A cross platform shortcut/symlink alternative.
-#[para] Unapologetically ugly - but practical in certain circumstances.
-#[para] A solution is required for application-driven filesystem links that survives cross platform moves as well as
-#[para] archiving and packaging systems.
-#[para] The target is specified in a minimally-encoded form in the filename itself - but still human readable.
-#[para] format of name #.fauxlink
-#[para] where can be empty - then the effective nominal name is the tail of the
-#[para] The file extension must be .fauxlink or .fxlnk
-#[para] The + symbol substitutes for forward-slashes.
-#[para] Other chars can be encoded using url-like encoding - (but only up to %7E !)
-#[para] We deliberately treat higher % sequences literally.
-#[para] This means actual uri::urn encoded unicode sequences (e.g %E2%99%A5 [lb]heart[rb]) can remain literal for linking to urls.
-#[para] e.g if an actual + or # is required in a filename or path segment they can be encoded as %2B & %23
-#[para] e.g a link to a file file#A.txt in parent dir could be:
-#[para] file%23A.txt#..+file%23A.txt.fauxlink
-#[para] or equivalently (but obviously affecting sorting) #..+file%23A.txt.fauxlink
-#[para] The can be unrelated to the actual target
-#[para] e.g datafile.dat#..+file%23A.txt.fauxlink
-#[para] This system has no filesystem support - and must be completely application driven.
-#[para] This can be useful for example in application test packages which may be tarred or zipped and moved cross platform.
-#[para] The target being fully specified in the name means the file doesn't have to be read for the target to be determined
-#[para] Extensions to behaviour should be added in the file as text data in Toml format,
-#[para] with custom data being under a single application-chosen table name
-#[para] The toplevel Toml table [lb]fauxlink[rb] is reserved for core extensions to this system.
-#[para] Aside from the 2 used for delimiting (+ #)
-#[para] certain characters which might normally be allowed in filesystems are required to be encoded
-#[para] e.g space and tab are required to be %20 %09
-#[para] Others that require encoding are: * ? \ / | : ; " < >
-#[para] The nul character in raw form, when detected, is always mapped away to the empty string - as very few filesystems support it.
-#[para] Control characters and other punctuation is optional to encode.
-#[para] Generally utf-8 should be used where possible and unicode characters can often be left unencoded on modern systems.
-#[para] Where encoding of unicode is desired in the nominalname,encodedtarget,tag or comment portions it can be specified as %UXXXXXXXX
-#[para] There must be between 1 and 8 X digits following the %U. Interpretation of chars following %U stops at the first non-hex character.
-#[para] This means %Utest would not get any translation as there were no hex digits so it would come out as %Utest
-#
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-#https://learn.microsoft.com/en-us/troubleshoot/windows-client/networking/url-encoding-unc-paths-not-url-decoded
-# ie "//server/c/Program files" works but "//server/c/Program%20Files" is now treated by windows as a literal path with %20 in it.
-#Using fauxlink - a link would be:
-# "my-program-files#++server+c+Program%20Files.fauxlink"
-#If we needed the old-style literal %20 it would become
-# "my-program-files#++server+c+Program%2520Files.fauxlink"
-#
-# The file:// scheme on windows supposedly *does* decode %xx (for use in a browser)
-# e.g
-# pfiles#file%3a++++localhost+c+Program%2520files
-# The browser will work with literal spaces too though - so it could just as well be:
-# pfiles#file%3a++++localhost+c+Program%20files
-#windows may default to using explorer.exe instead of a browser for file:// urls though
-#and explorer doesn't want the literal %20. It probably depends what API the file:// url is to be passed to?
-#in a .url shortcut either literal space or %20 will work ie %xx values are decoded
-
-
-
-#*** !doctools
-#[section Overview]
-#[para] overview of fauxlink
-#[subsection Concepts]
-#[para] -
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Requirements
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-#*** !doctools
-#[subsection dependencies]
-#[para] packages used by fauxlink
-#[list_begin itemized]
-
-package require Tcl 8.6-
-#*** !doctools
-#[item] [package {Tcl 8.6-}]
-
-# #package require frobz
-# #*** !doctools
-# #[item] [package {frobz}]
-
-#*** !doctools
-#[list_end]
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-#*** !doctools
-#[section API]
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# oo::class namespace
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-namespace eval fauxlink::class {
- #*** !doctools
- #[subsection {Namespace fauxlink::class}]
- #[para] class definitions
- if {[info commands [namespace current]::interface_sample1] eq ""} {
- #*** !doctools
- #[list_begin enumerated]
-
- # oo::class create interface_sample1 {
- # #*** !doctools
- # #[enum] CLASS [class interface_sample1]
- # #[list_begin definitions]
-
- # method test {arg1} {
- # #*** !doctools
- # #[call class::interface_sample1 [method test] [arg arg1]]
- # #[para] test method
- # puts "test: $arg1"
- # }
-
- # #*** !doctools
- # #[list_end] [comment {-- end definitions interface_sample1}]
- # }
-
- #*** !doctools
- #[list_end] [comment {--- end class enumeration ---}]
- }
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# Base namespace
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-namespace eval fauxlink {
- namespace export {[a-z]*}; # Convention: export all lowercase
-
- #todo - enforce utf-8
-
- #literal unicode chars supported by modern filesystems - leave as is - REVIEW
-
-
- variable encode_map
- variable decode_map
- #most filesystems don't allow NULL - map to empty string
-
- #Make sure % is not in encode_map
- set encode_map [dict create\
- \x00 ""\
- { } %20\
- \t %09\
- + %2B\
- # %23\
- * %2A\
- ? %3F\
- \\ %5C\
- / %2F\
- | %7C\
- : %3A\
- {;} %3B\
- {"} %22\
- < %3C\
- > %3E\
- ]
- #above have some overlap with ctrl codes below.
- #no big deal as it's a dict
-
- #must_encode
- # + # * ? \ / | : ; " < > \t
- # also NUL to empty string
-
- # also ctrl chars 01 to 1F (1..31)
- for {set i 1} {$i < 32} {incr i} {
- set ch [format %c $i]
- set enc "%[format %02X $i]"
- set enc_lower [string tolower $enc]
- dict set encode_map $ch $enc
- dict set decode_map $enc $ch
- dict set decode_map $enc_lower $ch
- }
-
- variable must_encode
- set must_encode [dict keys $encode_map]
-
-
- #if they are in
-
- #decode map doesn't include
- # %00 (nul)
- # %2F "/"
- # %2f "/"
- # %7f (del)
- #we exlude the forward slash because we already have + for that - and multiple ways to specify it obscure intention.
- #
- set decode_map [dict merge $decode_map [dict create\
- %09 \t\
- %20 { }\
- %21 "!"\
- %22 {"}\
- %23 "#"\
- %24 "$"\
- %25 "%"\
- %26 "&"\
- %27 "'"\
- %28 "("\
- %29 ")"\
- %2A "*"\
- %2a "*"\
- %2B "+"\
- %2b "+"\
- %2C ","\
- %2c ","\
- %2D "-"\
- %2d "-"\
- %2E "."\
- %2e "."\
- %3A ":"\
- %3a ":"\
- %3B {;}\
- %3b {;}\
- %3D "="\
- %3C "<"\
- %3c "<"\
- %3d "="\
- %3E ">"\
- %3e ">"\
- %3F "?"\
- %3f "?"\
- %40 "@"\
- %5B "\["\
- %5b "\["\
- %5C "\\"\
- %5c "\\"\
- %5D "\]"\
- %5d "\]"\
- %5E "^"\
- %5e "^"\
- %60 "`"\
- %7B "{"\
- %7b "{"\
- %7C "|"\
- %7c "|"\
- %7D "}"\
- %7d "}"\
- %7E "~"\
- %7e "~"\
- ]]
- #Don't go above 7f
- #if we want to specify p
-
-
- #*** !doctools
- #[subsection {Namespace fauxlink}]
- #[para] Core API functions for fauxlink
- #[list_begin definitions]
- proc Segment_mustencode_check {str} {
- variable decode_map
- variable encode_map ;#must_encode
- set idx 0
- set err ""
- foreach ch [split $str ""] {
- if {[dict exists $encode_map $ch]} {
- set enc [dict get $encode_map $ch]
- if {[dict exists $decode_map $enc]} {
- append err " char $idx should be encoded as $enc" \n
- } else {
- append err " no %xx encoding available. Use %UXX if really required" \n
- }
- }
- incr idx
- }
- return $err ;#empty string if ok
- }
-
- proc resolve {link} {
- variable decode_map
- variable encode_map
- variable must_encode
- set ftail [file tail $link]
- set extension_name [string range [file extension $ftail] 1 end]
- if {$extension_name ni [list fxlnk fauxlink]} {
- set is_fauxlink 0
- #we'll process anyway - but return the result wrapped
- #This should allow deliberate erroring for the calling dict user if the extension difference is inadvertent
- #(e.g blindly processing all files in a folder that is normally only .fauxlink files - but then something added that happens
- # to have # characters in it)
- #It also means if someone really wants to use the fauxlink semantics on a different file type
- # - they can - but just have to access the results differently and take that (minor) risk.
- #error "fauxlink::resolve refusing to process link $link - file extension must be .fxlnk or .fauxlink"
- set err_extra "\nnonstandard extension '$extension_name' for fauxlink. (expected .fxlnk or .fauxlink) Check that the call to fauxlink::resolve was deliberate"
- } else {
- set is_fauxlink 1
- set err_extra ""
- }
- set linkspec [file rootname $ftail]
- # - any # or + within the target path or name should have been uri encoded as %23 and %2b
- if {[tcl::string::first # $linkspec] < 0} {
- set err "fauxlink::resolve '$link'. Link must contain a # (usually at start if name matches target)"
- append err $err_extra
- error $err
- }
- #The 1st 2 parts of split on # are name and target file/dir
- #If there are only 3 parts the 3rd part is a comment and there are no 'tags'
- #if there are 4 parts - the 3rd part is a tagset where each tag begins with @
- #and each subsequent part is a comment. Empty comments are stripped from the comments list
- #A tagset can be empty - but if it's not empty it must contain at least one @ and must start with @
- #e.g name.txt#path#@tag1@tag2#test###.fauxlink
- #has a name, a target, 2 tags and one comment
-
- #check namespec already has required chars encoded
- set segments [split $linkspec #]
- lassign $segments namespec targetspec
- #puts stderr "-->namespec $namespec"
- set nametest [tcl::string::map $encode_map $namespec]
- #puts stderr "-->nametest $nametest"
- #nothing should be changed - if there are unencoded chars that must be encoded it is an error
- if {[tcl::string::length $nametest] ne [tcl::string::length $namespec]} {
- set err "fauxlink::resolve '$link' invalid chars in name part (section prior to first #)"
- append err [Segment_mustencode_check $namespec]
- append err $err_extra
- error $err
- }
- #see comments below regarding 2 rounds and ordering.
- set name [decode_unicode_escapes $namespec]
- set name [tcl::string::map $decode_map $name]
- #puts stderr "-->name: $name"
-
- set targetsegment [split $targetspec +]
- #check each + delimited part of targetspec already has required chars encoded
- set pp 0 ;#pathpart index
- set targetpath_parts [list]
- foreach pathpart $targetsegment {
- set targettest [tcl::string::map $encode_map $pathpart]
- if {[tcl::string::length $targettest] ne [tcl::string::length $pathpart]} {
- set err "fauxlink::resolve '$link' invalid chars in targetpath (section following first #)"
- append err [Segment_mustencode_check $pathpart]
- append err $err_extra
- error $err
- }
- #2 rounds of substitution is possibly asking for trouble..
- #We allow anything in the resultant segments anyway (as %UXXXX... allows all)
- #so it's not so much about what can be encoded,
- # - but it makes it harder to reason about for users
- # In particular - if we map %XX first it makes %25 -> % substitution tricky
- # if the user requires a literal %UXXX - they can't do %25UXXX
- # the double sub would make it %UXXX -> somechar anyway.
- #we do unicode first - as a 2nd round of %XX substitutions is unlikely to interfere.
- #There is still the opportunity to use things like %U00000025 followed by hex-chars
- # and get some minor surprises, but using %U on ascii is unlikely to be done accidentally - REVIEW
- set pathpart [decode_unicode_escapes $pathpart]
- set pathpart [tcl::string::map $decode_map $pathpart]
- lappend targetpath_parts $pathpart
-
- incr pp
- }
- set targetpath [join $targetpath_parts /]
- if {$name eq ""} {
- set name [lindex $targetpath_parts end]
- }
- #we do the same encoding checks on tags and comments to increase chances of portability
- set tags [list]
- set comments [list]
- switch -- [llength $segments] {
- 2 {
- #no tags or comments
- }
- 3 {
- #only 3 sections - last is comment - even if looks like tags
- #to make the 3rd part a tagset, an extra # would be needed
- set comments [list [lindex $segments 2]]
- }
- default {
- set tagset [lindex $segments 2]
- if {$tagset eq ""} {
- #ok - no tags
- } else {
- if {[string first @ $tagset] != 0} {
- set err "fauxlink::resolve '$link' invalid tagset in 3rd #-delimited segment"
- append err \n " - must begin with @"
- append err $err_extra
- error $err
- } else {
- set tagset [string range $tagset 1 end]
- set rawtags [split $tagset @]
- set tags [list]
- foreach t $rawtags {
- if {$t eq ""} {
- lappend tags ""
- } else {
- set tagtest [tcl::string::map $encode_map $t]
- if {[tcl::string::length $tagtest] ne [tcl::string::length $t]} {
- set err "fauxlink::resolve '$link' invalid chars in tag [llength $tags]"
- append err [Segment_mustencode_check $t]
- append err $err_extra
- error $err
- }
- lappend tags [tcl::string::map $decode_map [decode_unicode_escapes $t]]
- }
- }
- }
- }
- set rawcomments [lrange $segments 3 end]
- #set comments [lsearch -all -inline -not $comments ""]
- set comments [list]
- foreach c $rawcomments {
- if {$c eq ""} {continue}
- set commenttest [tcl::string::map $encode_map $c]
- if {[tcl::string::length $commenttest] ne [tcl::string::length $c]} {
- set err "fauxlink::resolve '$link' invalid chars in comment [llength $comments]"
- append err [Segment_mustencode_check $c]
- append err $err_extra
- error $err
- }
- lappend comments [tcl::string::map $decode_map [decode_unicode_escapes $c]]
- }
- }
- }
-
- set data [dict create name $name targetpath $targetpath tags $tags comments $comments fauxlinkextension $extension_name]
- if {$is_fauxlink} {
- #standard .fxlnk or .fauxlink
- return $data
- } else {
- #custom extension - or called in error on wrong type of file but happened to parse.
- #see comments at top regarding is_fauxlink
- #make sure no keys in common at top level.
- return [dict create\
- linktype $extension_name\
- note "nonstandard extension returning nonstandard dict with result in data key"\
- data $data\
- ]
- }
- }
- variable map
-
- #default exclusion of / (%U2f and equivs)
- #this would allow obfuscation of intention - when we have + for that anyway
- proc decode_unicode_escapes {str {exclusions {/ \n \r \x00}}} {
- variable map
- set ucstart [string first %U $str 0]
- if {$ucstart < 0} {
- return $str
- }
- set max 8
- set map [list]
- set strend [expr {[string length $str]-1}]
- while {$ucstart >= 0} {
- set s $ucstart
- set i [expr {$s +2}] ;#skip the %U
- set hex ""
- while {[tcl::string::length $hex] < 8 && $i <= $strend} {
- set in [string index $str $i]
- if {[tcl::string::is xdigit -strict $in]} {
- append hex $in
- } else {
- break
- }
- incr i
- }
- if {$hex ne ""} {
- incr i -1
- lappend map $s $i $hex
- }
- set ucstart [tcl::string::first %U $str $i]
- }
- set out ""
- set lastidx -1
- set e 0
- foreach {s e hex} $map {
- append out [string range $str $lastidx+1 $s-1]
- set sub [format %c 0x$hex]
- if {$sub in $exclusions} {
- append out %U$hex ;#put it back
- } else {
- append out $sub
- }
- set lastidx $e
- }
- if {$e < [tcl::string::length $str]-1} {
- append out [string range $str $e+1 end]
- }
- return $out
- }
- proc link_as {name target} {
-
- }
-
- #proc sample1 {p1 args} {
- # #*** !doctools
- # #[call [fun sample1] [arg p1] [opt {?option value...?}]]
- # #[para]Description of sample1
- # return "ok"
- #}
-
-
-
-
- #*** !doctools
- #[list_end] [comment {--- end definitions namespace fauxlink ---}]
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# Secondary API namespace
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-namespace eval fauxlink::lib {
- namespace export {[a-z]*}; # Convention: export all lowercase
- namespace path [namespace parent]
- #*** !doctools
- #[subsection {Namespace fauxlink::lib}]
- #[para] Secondary functions that are part of the API
- #[list_begin definitions]
-
- #proc utility1 {p1 args} {
- # #*** !doctools
- # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]]
- # #[para]Description of utility1
- # return 1
- #}
-
-
-
- #*** !doctools
- #[list_end] [comment {--- end definitions namespace fauxlink::lib ---}]
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-#*** !doctools
-#[section Internal]
-namespace eval fauxlink::system {
- #*** !doctools
- #[subsection {Namespace fauxlink::system}]
- #[para] Internal functions that are not part of the API
-
-
-
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Ready
-package provide fauxlink [namespace eval fauxlink {
- variable pkg fauxlink
- variable version
- set version 0.1.1
-}]
-return
-
-#*** !doctools
-#[manpage_end]
-
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/flagfilter-0.3.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/flagfilter-0.3.tm
deleted file mode 100644
index 00f58e82..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/flagfilter-0.3.tm
+++ /dev/null
@@ -1,2717 +0,0 @@
-#package provide flagfilter [namespace eval flagfilter {list [variable version 0.2.3]$version}]
-#package provide [set ::pkg flagfilter-0.2.3] [namespace eval [lindex [split $pkg -] 0] {list [variable version [lindex [split $pkg -] 1][set ::pkg {}]]$version}]
-#
-#package provide [lindex [set pkg {flagfilter 0.2.3}] 0] [namespace eval [lindex $pkg 0] {list [variable version [lindex $pkg 1][set pkg {}]]$version}]
-package provide [lassign {flagfilter 0.3} pkg ver]$pkg [namespace eval $pkg[set pkg {}] {list [variable version $::ver[set ::ver {}]]$version}]
-
-#Note: this is ugly.. particularly when trying to classify flags that are not fully specified i.e raw passthrough.
-# - we can't know if a flag -x --x etc is expecting a parameter or not.
-#0.2.2 2023-03 JN - added %match% placeholder support. Can be added to the dispatch command to tell it what command was actually matched. e.g tell xxx.tcl script that it was xxx.tcl when we matched on *.tcl
-
-
-namespace eval flagfilter {
- package require oolib ;# make 'oolib::collection new' available
-
- proc do_errorx {msg {code 1}} {
- if {$::tcl_interactive} {
- error $msg
- } else {
- puts stderr "|>err $msg"
- exit $code
- }
- }
-
- proc do_error {msg {then error}} {
- set levels [list debug info notice warn error critical alert emergency]
- #note we exit or error out even if debug selected - as every do_error call is meant to interrupt code processing at the site of call
- #this is not just a 'logging' call even though it has syslog-like level descriptors
- lassign $then type code
- if {$code eq ""} {
- set code 1
- }
- set type [string tolower $type]
- if {$type in [concat $levels exit]} {
- puts -nonewline stderr "|$type> $msg\n"
- } else {
- puts -nonewline stderr "|flagfilter> unable to interpret 2nd argument to do_error: '$then' should be one of '$levels' or 'exit '\n"
- }
- flush stderr
- if {$::tcl_interactive} {
- #may not always be desirable - but assumed to be more useful not to exit despite request, to aid in debugging
- if {[string tolower $type] eq "exit"} {
- puts -nonewline stderr " (exit suppressed due to tcl_interactive - raising error instead)\n"
- if {![string is digit -strict $code]} {
- puts -nonewline stderr "|flagfilter> unable to interpret 2nd argument to do_error: '$then' should be: 'exit '\n"
- }
- }
- flush stderr
- return -code error $msg
- } else {
- if {$type ne "exit"} {
- return -code error $msg
- } else {
- if {[string is digit -strict $code]} {
- exit $code
- } else {
- puts -nonewline stderr "|flagfilter> unable to interpret 2nd argument to do_error: '$then' should be 'error' or 'exit '\n"
- flush stderr
- return -code error $msg
- }
- }
- }
- }
- proc scriptdir {} {
- set possibly_linked_script [file dirname [file normalize [file join [info script] ...]]]
- if {[file isdirectory $possibly_linked_script]} {
- return $possibly_linked_script
- } else {
- return [file dirname $possibly_linked_script]
- }
- }
-
-}
-
-package require overtype
-
-
-namespace eval flagfilter {
- namespace export get_one_flag_value
- #review. Tcl can handle args like: {-a -val1 -b -val2} as long as they pair up.
- #this will ignore flag-like values if they follow a -flag
- # positional values that happen to start with - can still cause issues
- #get_flagged_only can return an unpaired list if there are solos, or if it finds no value for the last flaglike element
- # e.g from input {something -x -y -z} we will get {-x -y -z}
- #
- #
-
- #flagfilter::get_flagged_only may not always get things right when looking at a values list with command processors
- #Even if all solos from commands are supplied in solodict - a flag might be solo only in the context of a particualar commandset
- #The proper way to get flagged values from an arglist is to run the full parser.
- #This then should be restricted to use for a specific subset of args where the supplied solodict is known to apply
- proc get_flagged_only {arglist solodict} {
- #solodict - solo flags with defaults
- set solo_accumulator [dict create] ;#if multiple instances of solo flag found - append defaults to the value to form a list as long as the number of occurrences
- #puts ">>>get_flagged_only input $arglist solodict:'$solodict'"
- set result [list]
- set last_was_flag 0
- set result [list]
- set a_idx 0
- set end_of_options 0
- foreach a $arglist {
- if {$a eq "--"} {
- break
- }
- if {[dict exists $solodict $a]} {
- set last_was_flag 0
- if {[dict exists $solo_accumulator $a]} {
- set soloval [concat [dict get $solo_accumulator $a] [dict get $solodict $a]]
- } else {
- set soloval [dict get $solodict $a]
- }
- dict set solo_accumulator $a $soloval
- #we need to keep order of first appearance
- set idx [lsearch $result $a]
- if {$idx < 0} {
- lappend result $a $soloval
- } else {
- lset result $idx+1 $soloval
- }
- } else {
- if {!$last_was_flag} {
- if {$a eq "--"} {
-
- } else {
- if {[lindex $arglist $a_idx-1] eq "--"} {
- #end of options processing - none of the remaining are considered flags/options no matter what they look like
- set last_was_flag 0
- break
- } else {
- if {[string match -* $a]} {
- set last_was_flag 1
- lappend result $a ;#flag
- } else {
- #last wasnt, this isn't - don't output
- set last_was_flag 0
- }
- }
- }
- } else {
- #we only look for single leading - in the value if last wasn't a flag - but we give -- and soloflags special treatment.
- if {$a eq "--"} {
- #last was flag
- set last_was_flag 0
- } else {
- lappend result $a ;#value
- set last_was_flag 0
- }
- }
- }
- incr a_idx
- }
- if {([llength $result] % 2) != 0} {
- set last [lindex $result end]
- if {[string match -* $last] && ($last ni [dict keys $solodict])} {
- lappend result 1
- }
- }
- #puts ">>>get_flagged_only returning $result"
- return $result
- }
-
-
- ## get_one_paired_flag_value
- #best called with 'catch' unless flag known to be in arglist
- #raises an error if no position available after the flag to retrieve value
- #raises an error if flag not like -something
- #raises an error if flag not found in list
- proc get_one_paired_flag_value {arglist flag} {
- if {![regexp -- {-{1}[^-]+|-{2}[^-]+} $flag]} {
- #regexp excludes plain - and --
- #if {![string match -* $flag]} {}
- error "get_one_flag_value flag $flag does not look like a flag. Should be something like -$flag or --$flag"
- }
- set cindex [lsearch $arglist $flag]
- if {$cindex >= 0} {
- set valueindex [expr {$cindex + 1}]
- if {$valueindex < [llength $arglist]} {
- #puts stderr "++++++++++++++++++ get_one_flag_value flag '$flag' returning [lindex $arglist $valueindex]"
- return [lindex $arglist $valueindex]
- } else {
- error "flagfilter::get_one_paired_flag_value no value corresponding to flag $flag (found flag, but reached end of list)"
- }
- } else {
- error "flagfilter::get_one_paired_flag_value $flag not found in arglist: '$arglist'"
- }
- }
-}
-
-namespace eval flagfilter::obj {
-
-}
-
-
-namespace eval flagfilter {
- variable run_counter 0 ;#Used by get_new_runid to form an id to represent run of main check_flags function.
- #used as a basis for some object-instance names etc
- proc get_new_runid {} {
- variable run_counter
- if {[catch {package require Thread}]} {
- set tid 0
- } else {
- set tid [thread::id]
- }
- return "ff-[pid]-${tid}-[incr run_counter]"
- }
-
- namespace export check_flags
- proc do_debug {lvl debugconfig msg} {
- if {$lvl <= [dict get $debugconfig -debugargs]} {
- foreach ln [split $msg \n] {
- puts -nonewline stderr "|[dict get $debugconfig -source]> $ln\n"
- flush stderr
- }
- }
- }
-
- #----------------------------------------------------------------------
- # DO NOT RELY ON tcl::unsupported - it's named that for a reason and is not meant to be parsed
- #wiki.tcl-lang.org/page/dict+tips+and+tricks
- proc isdict {v} {
- if {[string match "value is a list *" [::tcl::unsupported::representation $v]]} {
- return [expr {!([llength $v] % 2)}]
- } else {
- return [string match "value is a dict *" [::tcl::unsupported::representation $v]]
- }
- }
-
- proc dict_format {dict} {
- dictformat_rec $dict "" " "
- }
- proc dictformat_rec {dict indent indentstring} {
- # unpack this dimension
- set is_empty 1
- dict for {key value} $dict {
- set is_empty 0
- if {[isdict $value]} {
- append result "$indent[list $key]\n$indent\{\n"
- append result "[dictformat_rec $value "$indentstring$indent" $indentstring]\n"
- append result "$indent\}\n"
- } else {
- append result "$indent[list $key] [list $value]\n"
- }
- }
- if {$is_empty} {
- #experimental..
- append result "$indent\n"
- #append result ""
- }
- return $result
- }
- #--------------------------------------------------------------------------
-
- #solo 'category' includes longopts with value
- #solo flags include the general list of -soloflags, and those specific to the current -commandprocessors spec (mashopts and singleopts)
- proc is_this_flag_solo {f solos objp} {
- if {![string match -* $f]} {
- #not even flaglike
- return 0
- }
-
-
- if {$f in $solos} {
- #review! - global -soloflags shouldn't override the requirements of a commandprocessor!
- #but.. each commandprocessor needs to understand global solos occuring before our match so that we classify correctly..
- #todo - this may need to reference v_map and current position in scanlist to do properly
- return 1
- }
- if {$f eq "-"} {
- #unless the caller declared it as a solo - treat this as a non flag element. (likely use is as a command match)
- return 0
- }
- if {$f eq "--"} {
- #this is it's own type endofoptions
- return 0
- }
-
- set p_opts [$objp get_combined_opts]
-
- set mashopts [dict get $p_opts mashopts]
- set singleopts [dict get $p_opts singleopts]
- set pairopts [dict get $p_opts pairopts]
- set longopts [dict get $p_opts longopts]
-
- if {$f in $singleopts} {
- return 1
- }
-
- #"any" keywords used by processors to consume anything - where we're not too worried about classifying a flagvalue vs an operand
- #examine these last so that an explicit configuration of flags as pairopts,mashopts etc can still be classified correctly
- if {"any" in $singleopts} {
- return 1
- }
- if {[string first "=" $f] >=1} {
- if {"any" in $longopts} {
- return 1
- }
- #todo foreach longopt - split on = and search
- }
-
- #Flag could still be part of a solo if it is in mashopts *and* has a value following it as part of the mash - but if it's a pairopt, but not mashable - we can rule it out now
- if {($f in $pairopts) && ($f ni $mashopts)} {
- return 0
- }
- #todo - suport mashes where one of the mashed flags takes an arg - review: only valid if it's last in the mash?
- #(presumably so - unless we there was some other value delimiter such as isnumeric or capitalised flags vs lowercase values - but that seems a step too far - would require some sort of mashspec/mash-strategy config)
- #last part of mash may actually be the value too. which complicates things
- #linux ls seems to do this for example:
- # ls -w 0
- # ls -lw 0
- # ls -lw0
- # also man.. e.g
- # man -Tdvi
- # man -Hlynx
- # man -H
- # - note this last one. '-H lynx' doesn't work - so it's a mashable opt that can take a value, but is not in pairopts! (-H with no value uses env value for browser)
- # see also comments in is_this_flag_mash
- #
-
- set flagletters [split [string range $f 1 end] ""]
- set posn 1
- set is_solo 1 ;#default assumption to disprove
- #trailing letters may legitimately not be in mashopts if they are part of a mashed value
- #we can return 0 if we hit a non-mash flag first.. but at each mashflag we need to test if we can classify as definitely solo or not, or else keep processing
- foreach l $flagletters {
- if {"-$l" ni $mashopts} {
- #presumably an ordinary flag not-known to us
- return 0
- } else {
- if {"-$l" in $pairopts} {
- if {$posn == [llength $flagletters]} {
- #in pairopts and mash - but no value for it in the mash - thefore not a solo
- return 0
- } else {
- #entire tail is the value - this letter is effectively solo
- return 1
- }
- } elseif {"-$l" in $singleopts} {
- #not allowed to take a value - keep processing letters
- } else {
- #can take a value! but not if at very end of mash. Either way This is a solo
- return 1
- }
- }
- }
- return $is_solo
- }
- #todo? support global (non-processor specific) mash list? -mashflags ?
- proc is_this_flag_mash {f objp} {
- if {![regexp -- {-{1}[^-]+|-{2}[^-]+} $f]} {
- #not even flaglike
- return 0
- }
- set optinfo [$objp get_combined_opts];#also applies to tail_processor - *usually* empty values for mashopts etc
-
- #we look at singleopts because even if the flag is in mashopts - when it is alone we don't classify it as a mash
- set singleopts pdict get $optinfo singleopts]
- if {$f in $singleopts} {
- return 0
- }
-
- set pairopts [dict get $optinfo pairopts]
- if {$f in [dict keys $pairopts]} {
- #here, the entire arg (f) we are testing is in pairopts - it could still however appear as part of a mash, with or without a trailing value, and with or without other flags before it in the mash (but if neither prefixed,nor tailed then obviously not a mash)
- return 0
- }
- set mashopts [dict get $optinfo mashopts]
- set flagletters [split [string range $f 1 end] ""]
- set is_mash 1 ;#to disprove - all letters must be in mashopts to consider it a mash.. unless trailing one also takes a value
- # .. in which case value could be at the tail of the mash.. or be the next arg in the list
- # We will take absense from singleopts and pairopts to indicate the mashflag *optionally* takes a value
- # (ie such a mashopt is a solo that can take a value only as a mashtail)
- # presence in pairopts indicates a mashflag must have a value
- # presense in singleopts indicates mashflag takes no value ever.
- # mashopt cannot be in both singleopts and pairopts. (NAND)
- foreach l $flagletters {
- if {-$l in $pairopts} {
- if {"-$l" in $mashopts} {
- #need to consider any remainder in the mash as this value .. if no remainder - then this is a mash, but not 'solo' because this flag needs to consume the following arg.
- # We are only concerned with mashness here so just stop processing mash elements when we hit the first one that is a pairopt
- break
- } else {
- #we require the pairopt to explicitly be listed in mashopts as well as pairopts if it is to be allowed to be part of a mash
- set is_mash 0
- }
- } elseif {"-$l" in $singleopts} {
- #singleopt & mashopt - cannot take a value, mashed or otherwise
- if {"-$l" ni $mashopts} {
- set is_mash 0
- }
- } else {
- if {"-$l" ni $mashopts} {
- set is_mash 0
- } else {
- #present only in mashopts - can take a value, but only immediately following in the mash
- break
- }
- }
- }
- return $is_mash
- }
- proc is_this_flag_for_me {f objp cf_args} {
- set processorname [$objp name]
- set optinfo [$objp get_combined_opts] ;#also applies to tail_processor - *usually* empty values for mashopts etc
-
- if {$processorname in [list "tail_processor"]} {
- return 1
- }
- if {$processorname in [list "global"]} {
- #todo - mashflags for global?
- set defaults [dict get $cf_args -defaults]
- set extras [dict get $cf_args -extras]
- set soloflags [dict get $cf_args -soloflags]
- if {$f in [concat $extras $soloflags [dict keys $defaults]]} {
- return 1
- }
- }
-
- set singleopts [dict get $optinfo singleopts]
- if {"any" in [string tolower $singleopts]} {
- #review semantics of 'all' here. does it mean any -xxx.. will match, or only if also in global -soloflags?
- return 1
- }
- set pairopts [dict get $optinfo pairopts]
- set allopts [concat $singleopts [dict keys $pairopts]]
- if {$f in $allopts} {
- return 1
- }
-
- #process mashopts last
- set mashopts [dict get $optinfo mashopts]
- if {"any" in [string tolower $mashopts]} {
- #if 'all' in mashopts - it can eat anything - review - is this even useful?
- return 1
- } else {
- set flagletters [split [string range $f 1 end] ""]
- set is_mash 1 ;#to disprove - all letters must be in mashopts to consider it a mash
- foreach l $flagletters {
- if {"-$l" ni $mashopts} {
- set is_mash 0
- }
- }
- return $is_mash
- }
-
- return 0
- }
-
-
-
- proc add_dispatch_raw {recordvar parentname v} {
- upvar $recordvar drecord
- if {[dict exists $drecord $parentname]} {
- set dispatchinfo [dict get $drecord $parentname raw]
- lappend dispatchinfo $v
- dict set drecord $parentname raw $dispatchinfo
- }
- }
- proc add_dispatch_argument {recordvar parentname k v} {
- upvar $recordvar drecord
- if {[dict exists $drecord $parentname]} {
- set dispatchinfo [dict get $drecord $parentname arguments]
- lappend dispatchinfo $k $v ;#e.g -opt 1
- dict set drecord $parentname arguments $dispatchinfo
- }
- }
- proc lsearch-all-stride-2 {l search} {
- set posns [lmap i [lsearch -all $l $search] {expr {($i % 2) == 0 ? $i : [list x]}}]
- return [lsearch -all -inline -not $posns x]
- }
- proc update_dispatch_argument {recordvar parentname k v} {
- upvar $recordvar drecord
- if {[dict exists $drecord $parentname]} {
- set dispatchinfo [dict get $drecord $parentname arguments]
- #can't assume there aren't repeat values e.g -v -v
- #dict set dispatchinfo $k $v
- if {[package vcompare [info tclversion] 8.7a5] >= 0} {
- set posns [lsearch -all -stride 2 $dispatchinfo $k]
- } else {
- set posns [lsearch-all-stride-2 $dispatchinfo $k]
- }
- set lastitem [lindex $posns end]
- if {[string length $lastitem]} {
- set val_idx [expr {$lastitem + 1}]
- set dispatchinfo [lreplace $dispatchinfo[set dispatchinfo {}] $val_idx $val_idx $v] ;# inlineK
- dict set drecord $parentname arguments $dispatchinfo
- } else {
- error "Unable to update dispatch argument $k with value $v in dispatch record for $parentname"
- }
- #dict set drecord $parentname $dispatchinfo
- }
- }
-
- #Note the difference between this and is_command_match.
- #Lack of a 'match' element does not cause a commandspec to skip allocating an operand it encounters
- #Note that this isn't a general test to be applied to the entire argument list.
- # - an arg may get matched by an earlier processor making it unavailable to be allocated by another processor
- # so this test only applies during the ordered examination of args
- proc can_this_commandspec_allocate_this_arg {flag cspec cf_args} {
- set cmdinfo [lindex $cspec 1]
- if {$cmdinfo eq "tail_processor"} {
- return 1
- }
- if {$cmdinfo eq "global"} {
- set defaults [dict get $cf_args -defaults]
- set soloflags [dict get $cf_args -soloflags]
- set extras [dict get $cf_args -extras]
- if {$flag in [concat $soloflags $extras [dict keys $defaults]]} {
- return 1
- }
- }
- if {![dict exists $cmdinfo match]} {
- return 1
- }
- set matchspeclist [dict get $cmdinfo match]
- foreach matchspec $matchspeclist {
- if {[regexp -- $matchspec $flag]} {
- return 1
- }
- }
- #only block it if there was a match pattern specified but it didn't match
- return 0
- }
- #Note - returns false for a cspec that has no match specified.
- #A command/subcommand with no match specification is allowed to allocate any value - so be careful with this
- # - it should not be used to *stop* an arg being allocated if the processor has no 'match' specified, or if it is another type of processor like 'tail_handler'.
- proc is_command_match {flag cspec} {
- set pinfo [lindex $cspec 1]
- if {[dict exists $pinfo match]} {
- set matchspeclist [dict get $pinfo match]
- foreach matchspec $matchspeclist {
- if {[regexp -- $matchspec $flag]} {
- return 1
- }
- }
- return 0
- } else {
- return 0
- }
- }
- proc is_command_match_any {f commandprocessors} {
- foreach comspec $commandprocessors {
- lassign $comspec cmdname cmdinfo
- if {[dict exists $cmdinfo match]} {
- set matchlist [dict get $cmdinfo match]
- foreach matchspec $matchlist {
- if {[regexp -- $matchspec $f]} {
- #actually a command
- return true
- }
- }
- }
- }
- return false
- }
-
- #determine if f is potentially a flag that takes a parameter from the next argument.
- #e.g --x=y (longopt) does not consume following arg but --something *might*
- proc is_candidate_toplevel_param_flag {f solos commandprocessors} {
- if {[is_command_match_any $f $commandprocessors]} {
- return false
- }
- if {$f in $solos} {
- return 0
- }
- if {$f in {- --}} {
- return 0
- }
- #longopts (--x=blah) and alternative --x blah
- #possibly also -x=blah
- if {[string match -* $f]} {
- if {[string first "=" $f]>1} {
- return 0
- }
- }
- return [expr {[string match -* $f]}]
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- #review - should we be using control::assert here?
- #It depends if this is intended to raise error at runtime - would using control::assert and disabling assertions cause problems?
- #todo - show caller info
- proc assert_equal {a b} {
- if {![expr {$a eq $b}]} {
- error "assert_equal $a $b"
- }
- }
-
-
-
-
-
- #{1 unallocated 2 unallocated 3 unallocated 4 unallocated 5 unallocated 6 unallocated} ;#initial v_map
- #1 2 3 4 5 6 ;#original list posns example
- # 2 6 ;#map_remaining example (scanlist)
- #1 3 4 5 ;#map_allocated example
- #{1 {cmd1 operand} 2 unallocated 3 {cmd2 operand} 4 {cmd2 flag} 5 {cmd2 flagvalue} 6 unallocated} ;#v_map updated example
- oo::class create class_vmap {
- variable o_map
- variable o_remaining
- variable o_allocated
- variable o_values
- variable o_codemap
- variable o_flagcategory
- constructor {values} {
- set o_codemap [dict create \
- operand op \
- flagvalue fv \
- soloflag so \
- flag fl \
- unallocated un \
- endofoptions eo \
- ]
- set o_flagcategory [list "flag" "flagvalue" "soloflag"]
- set o_values $values
- #set o_remaining [lsearch -all $values *] ;#create a list of indices e.g 0 1 2 3 4 5 6
- #lsearch -all * is fast for very small lists - but lseq wins from size around 30+
- if {[llength $values]} {
- if {[llength $values] < 30} {
- #common case is short lists - but we don't want to penalize large lists
- set o_remaining [lsearch -all $values *]
- } else {
- #punk::lib::range wraps lseq if available
- set o_remaining [punk::lib::range 0 [llength $values]-1]
- }
- } else {
- set o_remaining [list]
- }
- set o_allocated [list]
- set o_map [list]
- foreach posn $o_remaining {
- lappend o_map $posn unallocated
- }
- }
- method load {values rem alloc map} {
- set o_values $values
- set o_remaining $rem
- set o_allocated $alloc
- set o_map $map
- }
- method copy_to {obj} {
- $obj load $o_values $o_remaining $o_allocated $o_map
- }
- method update_map_from {obj} {
- #very basic sanity check first
- if {[llength $o_values] ne [llength [$obj get_values]]} {
- error "[self class].update_map_from cannot update. length of values mismatch"
- }
-
- set newmap [$obj get_map]
- }
-
- method get_codemap {} {
- return $o_codemap
- }
- method get_values {} {
- return $o_values
- }
- method get_remaining {} {
- return $o_remaining
- }
- method get_allocated {} {
- return $o_allocated
- }
- method get_map {} {
- return $o_map
- }
- method argnum_from_remaining_posn {scanlist_posn} {
- set vidx [lindex $o_remaining $scanlist_posn]
- if {![string is digit -strict $vidx]} {
- return -code error "[self class].argnum_from_remaining_posn cannot determine argnum from scanlist position:$scanlist_posn using unallocated list:'$o_remaining'"
- }
- return $vidx
- }
-
- method allocate {objp argnum type value} {
- set processorname [$objp name]
- if {$processorname eq "tail_processor"} {
- set owner "unallocated"
- } else {
- set owner [$objp parentname]
- }
- if {$argnum > [llength $o_values]-1} {
- return -code error "[self class].allocate cannot allocate argnum:$argnum. Only [llength $o_values] items in value list"
- }
- if {$argnum in $o_allocated} {
- return -code error "[self class].allocate already allocated '$processorname' argnum:'$argnum' type:'$type' val:'$value' remaining:$o_remaining allocated:$o_allocated map:$o_map"
- }
- lappend o_allocated $argnum
- set o_allocated [lsort -dictionary $o_allocated]
- dict set o_map $argnum [list $owner $type $value]
- set scanlist_posn [lsearch $o_remaining $argnum]
- set o_remaining [lreplace $o_remaining[set o_remaining {}] $scanlist_posn $scanlist_posn] ;#inlineK
-
-
-
- }
-
- method get_list_unflagged_by_class {classmatch} {
- set resultlist [list]
- dict for {k vinfo} $o_map {
- lassign $vinfo class type val
- if {[string match $classmatch $class]} {
- switch -- $type {
- flag - flagvalue - soloflag {}
- default {
- lappend resultlist $val
- }
- }
- }
- }
- return $resultlist
- }
-
- method get_list_flagged_by_class {classmatch} {
- set list_flagged [list]
- dict for {k vinfo} $o_map {
- lassign $vinfo class type val
- if {[string match $classmatch $class]} {
- switch -- $type {
- flag - flagvalue - soloflag {
- lappend list_flagged $val
- }
- }
- }
- }
- return $list_flagged
- }
-
- method get_merged_flagged_by_class {classmatch} {
- variable flagcategory
- set all_flagged [list]
- set seenflag [dict create] ;#key = -flagname val=earliest vindex
- dict for {k vinfo} $o_map {
- lassign $vinfo class type val
- if {[string match $classmatch $class]} {
- set a [llength $all_flagged] ;#index into all_flagged list we are building
- switch -- $type {
- soloflag {
- if {[dict exists $seenflag $val]} {
- set seenindex [dict get $seenflag $val]
- set seenindexplus [expr {$seenindex+1}]
- set existingvals [lindex $all_flagged $seenindexplus]
- lappend existingvals 1 ;#1 indicating presence - stored as list rather than a count. todo: consider global or per-solo options to support incrementing instead?
- lset all_flagged $seenindexplus $existingvals
- } else {
- dict set seenflag $val $a
- lappend all_flagged $val 1
- }
- }
- flag {
- if {![dict exists $seenflag $val]} {
- dict set seenflag $val $a
- lappend all_flagged $val
- }
- #no need to do anything if already seen - flagvalue must be next, and it will work out where to go.
- }
- flagvalue {
- set idxflagfor [expr {$k -1}]
- set flagforinfo [dict get $o_map $idxflagfor]
- lassign $flagforinfo ffclass fftype ffval
- #jn "--" following a flag could result in us getting here accidentaly.. review
- set seenindex [dict get $seenflag $ffval]
- if {$seenindex == [expr {$a-1}]} {
- #usual case - this is a flagvalue following the first instance of the flag
- lappend all_flagged $val
- } else {
- #write the value back to the seenindex+1
- set seenindexplus [expr {$seenindex+1}]
- set existingvals [lindex $all_flagged $seenindexplus]
- lappend existingvals $val ;#we keep multiples as a list
- lset all_flagged $seenindexplus $existingvals
- }
- }
- }
- }
- }
- return $all_flagged
- }
- method typedrange_class_type_from_arg {argclass argtype} {
- #set o_flagcategory [list "flag" "flagvalue" "soloflag"]
- if {$argclass eq "unallocated"} {
- switch -- $argtype {
- flag - flagvalue - soloflag {
- return [list unallocated flagtype]
- }
- default {
- if {![string length $argtype]} {
- #should only happen if something wrong with the tail_processor - rather than error out, for now at least make it stand out in the .
- set argtype UNKNOWN
- }
- return [list unallocated ${argtype}type] ;#e.g unallocated_operand, unallocated_endofoptions
- }
- }
- } else {
- return [list $argclass argtype] ;# e.g command something
- }
- }
-
- method get_ranges_from_classifications {classifications} {
- #puts stderr "get_ranges_from_classifications $classifications"
- #examine classifications and create a list of ranges
- set ranges [list];# e.g [list {unallocated 0 4} {cmd1 5 7} {unallocated 8 8} {cmd2 9 9} {cmd3 10 10} {unallocated 11 15}]
- set seen_commands [list]
- dict for {posn arginfo} $classifications {
- set is_new_cmd 0
- set is_sub_cmd 0
- set is_continuation 0
- set rangename [lindex $ranges end 0]
- set alloc [lindex $arginfo 0] ;#e.g of form 0 {unallocated operand} 1 {lscmd operand} 2 {lscmd soloflag} 3 {lscmd.dir operand} 4 {unallocated flag}
- set cmdname ""
- if {$alloc ne "unallocated"} {
- if {$alloc ni $seen_commands} {
- if {![llength $seen_commands]} {
- set cmdname $alloc
- set is_new_cmd 1
- } else {
- set tail [lindex $seen_commands end]
- if {$tail eq "unallocated"} {
- set cmdname $alloc
- set is_new_cmd 1
- } else {
- if {[string first . $alloc] >= 0} {
- set prefixcheck [lindex [split $alloc .] 0]
- if {![string equal -length [string length $prefixcheck] $prefixcheck $tail]} {
- #this is not unallocated, not a subcommand of the previous seen ie new command
- set cmdname $alloc
- set is_new_cmd 1
- } else {
- set cmdname $prefixcheck
- set is_sub_cmd 1
- set is_continuation 1
- }
- } else {
- set cmdname $alloc
- set is_new_cmd 1
- }
- }
- }
- } else {
- set cmdname $alloc
- set is_continuation 1
- }
- if {$is_continuation} {
- lassign [lindex $ranges end] _cmd n a b
- set ranges [lrange $ranges 0 end-1]
- lappend ranges [list command $n $a [incr b]]
- flagfilter::assert_equal $b $posn
- } elseif {$is_new_cmd} {
- lappend seen_commands $alloc
- if {$rangename eq ""} {
- lappend ranges [list command $cmdname $posn $posn]
- } else {
- lassign [lindex $ranges end] _cmd n a b
- lappend ranges [list command $cmdname [incr b] $posn]
- flagfilter::assert_equal $b $posn
- }
- } else {
- error "coding error during dispatch"
- }
- } else {
- if {$rangename eq ""} {
- lappend ranges [list unallocated mixed 0 0]
- } else {
- lassign [lindex $ranges end] class n a b
- if {$class eq "unallocated"} {
- #continuation - extend
- set ranges [lrange $ranges 0 end-1]
- lappend ranges [list unallocated mixed $a [incr b]]
- } else {
- #change from allocated to unallocated
- lappend ranges [list unallocated mixed [incr b] $posn]
- flagfilter::assert_equal $b $posn
- }
- }
- }
- }
- set rangesbytype [list]
- foreach oldrange $ranges {
- lassign $oldrange oldrangeclass oldrangetype A B ;#A,B for original range bounds, a,b for bounds of sub-ranges we are creating
- set last_type ""
- set newrangelist [list]
- set inner_range [list 0 0]
- if {$oldrangeclass ne "unallocated"} {
- #pass through - user can split commands further themselves by referencing the classifications map where each arg position is listed
- set last_type $oldrangeclass ;#note the deliberate slight misuse - we are using the 'class' here rather than the type as we aren't looking at types within a command range
- lappend rangesbytype $oldrange
- } else {
- #puts stdout "???????????????????????????????????????????????A$A B$B examining old range:'$oldrange'"
- for {set i $A} {$i <= $B} {incr i} {
- lassign [lindex $rangesbytype end] last_class last_type a b ;#enough just to use the type without the class
- set a_info [dict get $classifications $i]
- lassign $a_info argclass argtype v
- lassign [my typedrange_class_type_from_arg $argclass $argtype] newrangeclass newrangetype
- if {$last_type eq ""} {
- lappend rangesbytype [list "unallocated" $newrangetype 0 0]
- } else {
- if {$last_type eq $newrangetype} {
- set rangesbytype [lrange $rangesbytype 0 end-1]
- lappend rangesbytype [list $last_class $last_type $a $i]
- } else {
- lappend rangesbytype [list $newrangeclass $newrangetype $i $i]
- }
- }
- }
- }
- }
-
- return [list -ranges $ranges -rangesbytype $rangesbytype]
- }
-
- method grid {} {
- set posns [dict keys $o_map]
- set col1 [string repeat " " 15]
- set col [string repeat " " 4]
- set pline "[overtype::left $col1 {var indices}] "
- foreach p $posns {
- append pline [overtype::left $col $p]
- }
- set remline "[overtype::left $col1 {unallocated}] "
- foreach vidx $posns {
- if {$vidx ni $o_remaining} {
- append remline [overtype::left $col "."]
- } else {
- set tp [lindex [dict get $o_map $vidx] 1]
- #set tp [string map $o_codemap $tp]
- if {[dict exists $o_codemap $tp]} {
- set tp [dict get $o_codemap $tp]
- }
- append remline [overtype::left $col $tp]
- }
- }
- set cmdlist [list]
- dict for {vidx info} $o_map {
- if {[lindex $info 0] ne "unallocated"} {
- set c [lindex [split [lindex $info 0] .] 0]
- if {$c ni $cmdlist} {
- lappend cmdlist $c
- }
- }
- }
- set clinelist [list]
- foreach c $cmdlist {
- set cline "[overtype::left $col1 $c] "
- dict for {vidx info} $o_map {
- lassign $info class type v
- if {($c eq $class) || [string equal -length [string length "$c."] "$c." $class]} {
- #set tp [string map $o_codemap $type]
- if {[dict exists $o_codemap $type]} {
- set tp [dict get $o_codemap $type]
- }
- append cline [overtype::left $col $tp]
- } else {
- append cline [overtype::left $col "."]
- }
- }
- lappend clinelist $cline
- }
-
-
- set aline "[overtype::left $col1 {allocated}] "
- foreach vidx $posns {
- if {$vidx ni $o_allocated} {
- append aline [overtype::left $col "."]
- } else {
- set tp [lindex [dict get $o_map $vidx] 1]
- #set tp [string map $o_codemap $tp]
- if {[dict exists $o_codemap $tp]} {
- set tp [dict get $o_codemap $tp]
- }
- append aline [overtype::left $col $tp]
- }
- }
-
- return "$pline\n$remline\n[join $clinelist \n]\n$aline\n"
- }
-
- }
-
-
- #!todo - check if -commandprocessors members will collide with existing -flags in values before moving them
- #!todo - skip optional sub-flag value if the next arg following its parent is a flag i.e proper handling of -commandprocessors {cmd {cmd sub "default}} when only cmd supplied.
- #!important to fix. At the moment it could eat a further unflagged item in values later in the list which was intended for a different -commandprocessors member!
- #add support for -commandprocessors {-cmd {-cmd -othercmd "default"}} to be a safe way to specify a linked -flag move that does the same.
- proc allocate_arguments {PROCESSORS solos values cf_args caller} {
- set runid [lindex [split [namespace tail $PROCESSORS] _] 1] ;# objname is of form PROCESSORS_pid-threadid-counter where "PROCESSORS_" is a literal
- #puts stderr ">>>>>>> solos: $solos"
- dict set debugc -debugargs [dict get $cf_args -debugargs]
- dict set debugc -source "allocate_arguments $caller"
-
- set defaults [dict get $cf_args -defaults]
-
- set cmdprocessor_records [$PROCESSORS get_commandspecs]
-
-
- set sep "\uFFFE" ;#argument-subargument separator (choose something else if this causes problems.. but we want something unlikely (or preferably impossible?) to be in a commandline, ideally a single character, and which at least shows something on screen during debug)
- set sepstr "\\uFFFE" ;#for human readable error msg
- #\u001E was tried and doesn't output on some terminals)
-
- set remaining_unflagged [dict create]
-
- set extra_flags_from_positionals [list] ;#values moved to -values
- set moved_to_flagged [dict create]
-
- #implied_ are values supplied from defaults when a flag or operand was not found
- set implied_flagged [list]
- set implied_unflagged [list]
-
-
- set dispatch [dict create]
- #sanitize and raise error if sep somehow in values
- if {[string first $sep $cmdprocessor_records] >= 0} {
- do_error "allocate_arguments flags error separator '$sep' ($sepstr) found in values "
- }
- #--------------------------------------
- set VMAP [flagfilter::class_vmap create flagfilter::VMAP_$runid $values]
- #--------------------------------------
-
- set unconsumed_flags_and_values [list]
- set unflagged [dict create]
-
- ######################
- #main -commandprocessors loop which scans the valuelist
- set values_index 0 ;#track where we are up to as we allocate values to unflagged elements
- set source_values $values ;#start with all including -flagged
-
- #todo - get rid of most of these flags last_x_was etc - and just do lookups into the v_map
- # as this will probably involve *lots* of small functiona calls - keep this boolean version and check for performance issues.
- set a_index 0
- set is_args_flag 0
- set last_arg_was_paramflag 0 ;#a flag that expects a parameter to follow
- set last_arg_was_solo 0
- set solo_flags [dict keys $solos] ;#solos is a dict of -flag (preprocessed)
- set end_of_options 0
- set end_of_options_index -1 ;#as later processors can rescan - we need to make sure they only look at arguments after this point
- set last_p_found [dict create by "" index "" item ""]
- set sequence 0
- set argerrors [list] ;#despite being a list - we will break out at first entry and return for now.
- set parsestatus "ok"
-
- #set LAUNCHED [oolib::collection create col_processors_launched_$runid]
- #set MATCHED [oolib::collection create col_processors_matched_$runid]
- #oo::objdefine col_processors_matched_$runid {
- # method test {} {
- # return 1
- # }
- #}
-
- #set objp [$PROCESSORS object_from_record $p] ;#temp convenience
-
- foreach objp [$PROCESSORS items] {
- set objparent [$objp parent]
- #$LAUNCHED add $objp [$objp name]
- set p [$objp get_def] ;#individual record e.g {mycmd {match run singleopts {-x}}} or {tail_processor {}}
-
- lassign $p parentname pinfo
- set is_sub [$objp is_sub] ;#is subargument - should look to see if last related spec got a value and abort if not.
- set is_p_flag [$objp is_flag] ;#sub can be a flag even if parent isn't
- set processorname [$objp name]
- if {[$objp is_sub]} {
- if {![[$objp parent] found_match]} {
- continue
- }
- set p_sub [dict get $pinfo sub]
- }
- do_debug 3 $debugc " =========================>> p $p sequence:$sequence a_index $a_index"
-
- if {$processorname in [list "global" "tail_processor"]} {
- dict set last_p_found by $processorname
- #dict set last_p_found index $a_index
- #dict set last_p_found item $a
- }
- # -format {x {sub y default "default"}} means y is dependent on x being present and shouldn't eat if the next value isn't flaglike
- # -format {-x {sub -y}} does the same for moving positionals to the flagged list.
-
-
- #set remaining_values [lrange $source_values $a_index end]
- #####################################
- # full rescans for later processors
- set remaining_values $source_values ;#source_values shrinks as commands take arguments
- set a_index 0
- #####################################
-
- do_debug 3 $debugc "-------->________>p '$processorname' remaining vals $remaining_values"
-
- #!todo - use v_map as an extra determinant to stop sequence for a command-set. (don't extend beyond where args have already been snipped by another command)
- if {[$objp name] eq "tail_processor"} {
- set mapcopy [flagfilter::class_vmap new {}] ;#no need to supply values as we are copying data from $VMAP
- $VMAP copy_to $mapcopy
- $objp set_map_object $mapcopy
- } else {
- $objp set_map_object $VMAP
- }
- foreach a $remaining_values {
- set argnum [[$objp get_map_object] argnum_from_remaining_posn $a_index]
- if {![string is integer -strict $argnum]} {
- error "arg '$a' scan_index:$a_index - calculated argnum:'$argnum' is invalid"
-
- }
- set sub_operand 0
- do_debug 3 $debugc "$argnum >eoptions_idx:$end_of_options_index a_index:$a_index __________________________________________________________a $a"
- if {$end_of_options_index > -1} {
- set end_of_options [expr {$a_index >= $end_of_options_index}]
- }
-
- #review - data with leading - may be unintentionally interpreted as a flag
- if {[string trim $a] eq "--"} {
- #generally means end of options processing..
- #review - pass -- through??
- set last_arg_was_paramflag 0 ;#we don't treat first arg following end_of_options as belonging to the flag! - it is potentially an operand to the command
- set is_solo_flag 0
- set end_of_options 1
- set end_of_options_index $a_index
- #if {[lindex $p 0] eq "tail_processor"} {
- $objp allocate $argnum "endofoptions" $a
- set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK
- incr a_index -1
- #}
- } else {
- if {($last_arg_was_paramflag) && ([$objp arg_is_defined_solo_to_me $a])} {
- #last flag expecting param - but this flag *known* to be solo
- #keep it simple and break out at first solo_flag related error ...unless it is trailing flag in the list
- lappend argerrors [list flagerror solo_flag_following_non_solo_flag bad_flag $a]
- set last_arg_was_solo 1
- break
- }
- #set is_solo_flag [expr {($a in $solo_flags)}]
- #set is_solo_flag [is_this_flag_solo $a $solo_flags $objp]
- set is_solo_flag [$objp arg_is_defined_solo_to_me $a]
-
- if {!$end_of_options} {
- if {!$last_arg_was_paramflag} {
- if {!$is_solo_flag} {
- set is_args_flag [is_candidate_toplevel_param_flag $a $solo_flags $cmdprocessor_records]
- #set is_args_flag [string match -* $a]
- }
- if {$is_args_flag || $is_solo_flag} {
- if {[dict get $last_p_found by] eq $processorname} {
- if {![is_this_flag_for_me $a $objp $cf_args]} {
- if {$processorname ne "globalXXX"} {
- do_debug 3 $debugc "----breaking--- $processorname already found a value [dict get $last_p_found item] and has now hit an unrecognized option: $a"
- break
- }
- }
- }
- }
- } else {
- #last was flag expecting a param
- set is_args_flag 0
- set is_solo_flag 0
- }
- } else {
- #end_of_options - ignore solo and other flags now.
- set is_args_flag 0
- set is_solo_flag 0
- set last_arg_was_paramflag 0
-
- }
-
- #puts stderr "!!!!!!!!!!!!!!!!!!1 here is_args_flag:$is_args_flag"
- do_debug 3 $debugc " >________>________>is_p_flag: $is_p_flag last_arg_was_paramflag:$last_arg_was_paramflag is_args_flag:$is_args_flag is_solo: $is_solo_flag (soloflags:$solo_flags) a:$a "
- if {!$is_args_flag && !$is_solo_flag } {
-
- if {!$last_arg_was_paramflag} {
- if {[dict get $last_p_found by] eq $processorname} {
- if {$processorname ne "tail_processor"} {
- #we already found our unflagged value - and now we've hit another - time to break and hand it to a subcommand processor if any
- do_debug 3 $debugc "----breaking--- $processorname already found a value [dict get $last_p_found item] and has now hit another value: $a"
- break
- }
- }
- set sequence_ok 1 ;#default assumption
- set can_allocate [can_this_commandspec_allocate_this_arg $a $p $cf_args]
-
- if {$can_allocate} {
- if {$is_sub} {
- #!todo - use v_map as sequence terminator
- #check if our find is in sequence
- #we are only hunting non-flagged items and the the previous finder removes 1 from the source_values list
- #therefore the a_index of our find should be the same if we are processing the very next argument.
- #we have already checked that it was a related entity which found the last one.
- #todo - review if it matters when parents/siblings don't eat all the way up to the next -flag.
- #todo - implement a 'gather' key to keep eating in sequence and accumulate the values as a list
- if {$a_index > [dict get $last_p_found index]} {
- do_debug 3 $debugc "OUT OF SEQUENCE a_index:$a_index vs last_found index:[dict get $last_p_found index], $processorname disengaging - ignoring value $a and leaving it to the next processor"
- set last_arg_was_paramflag 0
- do_debug 3 $debugc "<--- breaking --->"
- break
- } elseif {$a_index < [dict get $last_p_found index]} {
- #too early.... found something before previous match
- do_debug 3 $debugc "+++++++++++++++out of sequence $processorname - too early.. keeping scanning"
- set sequence_ok 0
- }
- if {$sequence_ok} {
- set sub_operand 1
- }
- }
- }
-
- if {$can_allocate && $sequence_ok} {
- #found a non-flagged value in the argumentlist to either reallocate to flagged values or to positional values
- if {[dict exists $pinfo dispatch]} {
- if {!$is_sub} {
- #this must be the arg that caused the match
- dict set dispatch $parentname [list command [dict get $pinfo dispatch] matched $a arguments [list] raw [list]]
- } else {
- #todo
- lappend argerrors [list unsupported_dispatch $processorname]
- }
- }
- if {$sub_operand} {
- if {[dict exists $dispatch $parentname]} {
- #todo - defaults?
- add_dispatch_argument "dispatch" $parentname $processorname $a
- add_dispatch_raw "dispatch" $parentname $a
- } else {
- #warning?
- #lappend argerrors [list subcommand_unable_to_add_operand $processorname]
- do_debug 3 $debugc "subcommand $processorname aborting scanning because parent command wasn't activated"
- break
- }
- }
- do_debug 2 $debugc " >+++++++>++++++++>++++++++>setting $processorname [if {$is_p_flag} {list -} {}]value $a"
- if {$processorname eq "tail_processor"} {
- set argnum [[$objp get_map_object] argnum_from_remaining_posn $a_index]
- set argname arg$argnum
- lappend remaining_unflagged $argname $a
- lappend unconsumed_flags_and_values $a
- dict set unflagged $argname $a
- } elseif {$is_p_flag} {
- $objp set_matched_argument $argnum $a
- if {$is_sub} {
- dict set extra_flags_from_positionals $p_sub $a
- } else {
- dict set extra_flags_from_positionals $parentname $a
- }
- lappend moved_to_flagged $processorname $a
- #if has dependent commands ? - check for deep subcommand match?
- } else {
- $objp set_matched_argument $argnum $a
- #lappend positional_values $a
- dict set unflagged $processorname $a
- }
- do_debug 4 $debugc " >________>________>________>source_values :'$source_values'"
- do_debug 3 $debugc " >________>________>________>source_values len:[llength $source_values] removing element $a_index val:[lindex $source_values $a_index]"
-
- #----------------------------
- dict set last_p_found by $processorname
- dict set last_p_found index $a_index
- dict set last_p_found item $a
- #------------------------------
- $objp allocate $argnum "operand" $a
- set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK
- incr values_index ;#only increment when we allocate a value to one of the members of -commandprocessors
- set last_arg_was_paramflag 0
- if {$processorname ne "tail_processor"} {
- #don't break until we hit an unrecognized flag or another unflagged value
- incr a_index -1
- #don't increment a_index before break, because we have shortened the list by 1.
- #do_debug 3 $debugc "----breaking---"
- #break
- } else {
- #decrement to compensate for shortened list because tail_processor continues to end
- incr a_index -1
- }
- }
-
- } else {
- #last_arg_was_paramflag
- set lastarg [dict get $last_p_found item]
- #puts stdout "+++ lastarg: $lastarg source_values: [dict get $last_p_found source_values] a_index: $a_index"
- if {$processorname eq "tail_processor"} {
- lappend unconsumed_flags_and_values $a
- }
- if {([dict get $last_p_found by] eq $processorname) && [is_this_flag_for_me $lastarg $objp $cf_args]} {
- update_dispatch_argument "dispatch" $parentname $lastarg $a
- add_dispatch_raw "dispatch" $parentname $a
- dict set last_p_found by $processorname
- dict set last_p_found index $a_index
- dict set last_p_found item $a
- $objp allocate $argnum "flagvalue" $a
- set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK
- incr a_index -1
- }
- set last_arg_was_paramflag 0
- }
- } else {
- # is a flag of some sort ({!$is_args_flag && !$is_solo_flag} = false)
- if {$processorname eq "tail_processor"} {
- lappend unconsumed_flags_and_values $a
- }
- if {([dict get $last_p_found by] eq $processorname) && [is_this_flag_for_me $a $objp $cf_args]} {
- if {$is_solo_flag} {
- add_dispatch_argument "dispatch" $parentname $a 1
- add_dispatch_raw "dispatch" $parentname $a
- set last_arg_was_solo 1
- set last_arg_was_paramflag 0
- $objp allocate $argnum "soloflag" $a
- } else {
- add_dispatch_argument "dispatch" $parentname $a ""
- add_dispatch_raw "dispatch" $parentname $a
- set last_arg_was_solo 0
- set last_arg_was_paramflag 1
- $objp allocate $argnum "flag" $a
- }
- dict set last_p_found by $processorname
- dict set last_p_found index $a_index
- dict set last_p_found item $a
- do_debug 4 $debugc " >2_______>________>________>source_values :'$source_values'"
- do_debug 3 $debugc " >2_______>________>________>source_values len:[llength $source_values] removing element $a_index val:[lindex $source_values $a_index]"
- set source_values [lreplace $source_values[set source_values {}] $a_index $a_index] ;#inlineK
- incr a_index -1
- } else {
- #auto alternate based on last value.. unless end_of_options
- if {!$end_of_options} {
- if {$a in $solo_flags} {
- set last_arg_was_solo 1
- set last_arg_was_paramflag 0
- } else {
- set last_arg_was_paramflag 1
- }
- }
- if {$a_index eq ([llength $source_values]-1)} {
- #puts "XXXXXXXXXXXXXXXXXXX $a_index source_values:'$source_values'"
- #if at end of list don't retain any last..was info.
- set last_arg_was_solo 0
- set last_arg_was_paramflag 0
- }
- #skip - don't eat
- }
- }
- }
- incr a_index
- }
-
- if {![$objp found_match]} {
-
- #after break - we have retained vars: $parent, $sub_operand $pinfo $processorname etc
- #didn't find an unflagged var - set a default if one was specified.
- #do nothing otherwise - check_args will determine if it was -required etc.
- #review - should only apply if parent cmd found something?
- if {[dict exists $pinfo default]} {
- set defaultval [dict get $pinfo default]
- if {$is_p_flag} {
- if {$is_sub} {
- dict set extra_flags_from_positionals $p_sub $defaultval
- } else {
- dict set extra_flags_from_positionals $processorname $defaultval
- }
- #lappend moved_to_flagged $processorname $defaultval
- lappend implied_flagged $processorname $defaultval
- do_debug 3 $debugc "SETTING DEFAULT varname:$processorname $defaultval implied_flagged: $implied_flagged "
- } else {
- lappend implied_unflagged $processorname $defaultval
- dict set unflagged $processorname $defaultval
- do_debug 3 $debugc "SETTING DEFAULT varname:$processorname $defaultval moved_to_flagged: $moved_to_flagged "
- }
-
- if {$is_sub && !$sub_operand} {
- if {[dict exists $dispatch $parentname]} {
- add_dispatch_argument "dispatch" $parentname $processorname $defaultval
- } else {
- lappend argerrors [list subcommand_unable_to_add_default_operand $processorname $defaultval]
- }
- }
- }
- }
-
- if {[$objp name] eq "tail_processor"} {
- $VMAP update_map_from [$objp get_map_object]
- }
-
- if {[llength $argerrors]} {
- set parsestatus "error"
- #abort processing at first error - we won't be able to make sense of the remaining args anyway
- #even the tail_processor won't be able to classify reliably because flag meanings depend on the configured commands
- break
- }
- }
-
- #assertion - should be none?
- #set remaining_values [lrange $source_values $a_index end]
- #do_debug 3 $debugc "-------->________>end of processing - remaining vals $remaining_values"
-
- do_debug 2 $debugc "========>=========>originals : $values"
- do_debug 2 $debugc "[$VMAP get_map]"
- do_debug 2 $debugc "========>=========>unconsumed: $unconsumed_flags_and_values"
-
-
-
-
-
- set all_flagged [$VMAP get_merged_flagged_by_class *]
- set all_flagged_plus [concat $all_flagged $extra_flags_from_positionals]
-
- set all_flagged_list [$VMAP get_list_flagged_by_class *]
- set all_flagged_list [concat $all_flagged_list $extra_flags_from_positionals]
-
- set remaining_flagged [$VMAP get_merged_flagged_by_class "unallocated"]
-
- set remaining_flagged_list [$VMAP get_list_flagged_by_class "unallocated"]
-
-
- set unflagged_list_in_processing_order [dict values $unflagged]
- set unflagged_list [$VMAP get_list_unflagged_by_class *]
-
- set unflagged_list_remaining [$VMAP get_list_unflagged_by_class "unallocated"]
-
- return [dict create \
- listremaining $unconsumed_flags_and_values \
- parseerrors $argerrors \
- parsestatus $parsestatus \
- flagged $all_flagged_plus \
- flaggedlist $all_flagged_list \
- flaggedremaining $remaining_flagged \
- flaggedlistremaining $remaining_flagged_list \
- unflagged $unflagged \
- unflaggedlist $unflagged_list \
- unflaggedremaining $remaining_unflagged \
- unflaggedlistremaining $unflagged_list_remaining \
- flaggednew $extra_flags_from_positionals \
- arglist [concat $unflagged_list_in_processing_order $all_flagged] \
- arglistremaining [concat $unflagged_list_remaining $remaining_flagged] \
- impliedflagged $implied_flagged \
- impliedunflagged $implied_unflagged \
- dispatch $dispatch \
- classifications [$VMAP get_map] \
- gridstring "\n[$VMAP grid]" \
- vmapobject "flagfilter::VMAP_$runid" \
- ]
- }
-
-
-
-
-
-
-
-
-
-
-
- #specialisation for collection class to contain commandprocessors
- # we expect to use only a single instance of this
- oo::class create col_allprocessors {
- superclass oolib::collection
- variable o_commandspecs
- method add_processor {p} {
- my add $p [$p name]
- if {[$p is_sub]} {
- set parentname [$p parentname]
- set obj_parent [my item $parentname]
- set col_siblings [$obj_parent children]
- $col_siblings add $p [$p name]
- }
- }
- method set_commandspecs {cspecs} {
- set o_commandspecs $cspecs
- }
- method get_commandspecs {} {
- set o_commandspecs
- }
- #treating as singleton.. todo tidy
- method name_from_record {rec} {
- lassign $rec parentname pinfo
- if {[dict exists $pinfo sub]} {
- set name [join [list $parentname [dict get $pinfo sub]] .]
- } else {
- set name $parentname
- }
- return $name
- }
- method object_from_record {rec} {
- set name [my name_from_record $rec]
- return [my item $name]
- }
- #basic check if arg may consume the following one - not based on any specific info from processors
- method arg_appears_standalone {f} {
- if {(![string match "-*" $f]) && (![string match "/*" $f])} {
- #not even flaglike
- return 1
- }
- if {$f in {- --}} {
- return 1
- }
- }
- #does any processor define it as solo
- method flag_can_be_solo {f} {
- foreach objp [my items] {
- if {[$objp arg_is_defined_solo_to_me $f]} {
- return 1
- }
- }
- return 0
- }
- }
- oo::class create col_parents {
- superclass oolib::collection
- method add_parent {p} {
- if {[$p is_sub]} {
- error "cannot add a sub-processor to the main parents collection"
- }
- my add $p [$p name]
- }
- }
- #each parent processor has a children collection which can only accept processors with sub defined.
- oo::class create col_childprocessors {
- superclass oolib::collection
- variable o_ownername
- method set_owner {parentname} {
- set o_ownername $parentname
- }
- #owner of the collection (a parent processor)
- method owner {} {
- return $o_ownername
- }
- method add_processor {p} {
- if {![$p is_sub]} {
- error "processor must have 'sub' element to add to the parent's collection"
- }
- #check name matches this parent..
-
- my add $p [$p name]
- }
- }
-
- #todo - rename 'cprocessor' is misleading
- oo::class create cprocessor {
- variable o_runid
- variable o_name
- variable o_definition
- variable o_pinfo
- variable o_parentname
- variable o_is_sub
- variable o_col_children
- variable o_mashopts
- variable o_singleopts
- variable o_pairopts
- variable o_longopts
- variable o_found_match ;#we directly matched a command trigger or positional argument
- variable o_matched_argument
- variable o_matched_argnum
- variable o_matchspec
- variable o_vmap
- constructor {definition runid} {
- set o_vmap ""
- set o_definition $definition
- set o_runid $runid
- if {([llength $o_definition] < 2) || ([llength [lindex $o_definition 0]] != 1)} {
- error "[self class].constructor Unable to interpret definition '$o_definition'"
- }
- lassign $o_definition o_parentname o_pinfo
- if {([llength $o_pinfo] %2) != 0} {
- error "[self class].constructor second element of definition '$o_definition' not a dict"
- }
- set o_is_sub [dict exists $o_pinfo sub]
- if {!$o_is_sub} {
- set o_name $o_parentname
- set o_col_children [::flagfilter::col_childprocessors new]
- $o_col_children set_owner $o_name
- } else {
- set o_name [join [list $o_parentname [dict get $o_pinfo sub]] .]
- }
- if {[dict exists $o_pinfo match]} {
- set o_matchspec [dict get $o_pinfo match]
- } else {
- #review - unix paths? conflict with windows style flag such as /w
- #must accept empty string
- set o_matchspec {^[^-^/].*|^$} ;#match anything that isn't flaglike
- }
- set o_found_match 0
- set o_matched_argument "" ;#need o_found_match to differentiate match of empty string
- set o_matched_argnum -1
- #load mashopts etc at construction time as they're static
- set o_mashopts [list]
- set o_singleopts [list]
- set o_pairopts [list]
- set o_longopts [list]
- if {[dict exists $o_pinfo mashopts]} {
- lappend o_mashopts {*}[dict get $o_pinfo mashopts]
- }
- if {[dict exists $o_pinfo singleopts]} {
- lappend o_singleopts {*}[dict get $o_pinfo singleopts]
- }
- if {[dict exists $o_pinfo pairopts]} {
- lappend o_pairopts {*}[dict get $o_pinfo pairopts]
- }
- if {[dict exists $o_pinfo longopts]} {
- lappend o_longopts {*}[dict get $o_pinfo longopts]
- }
- }
- destructor {
- catch {$o_vmap destroy}
- if {!$o_is_sub} {
- $o_col_children destroy
- }
- }
-
- method name {} {
- return $o_name
- }
- #open things up during oo transition..
- method get_def {} {
- return $o_definition
- }
- method is_flag {} {
- if {[my is_sub]} {
- #sub can be a flag even if parent isn't
- set subname [dict get $o_pinfo sub]
- return [string match -* $subname]
- } else {
- return [string match -* $o_name]
- }
- }
- method has_same_parent {other} {
- return [expr {[other parentname] eq $o_parentname}]
- }
- method is_sub {} {
- return $o_is_sub
- }
-
- method set_map_object {map} {
- set o_vmap $map
- }
- method get_map_object {} {
- return $o_vmap
- }
- method allocate {argnum type val} {
- if {$o_vmap eq ""} {
- error "[self class].allocate ($o_name) vmap is not set."
- }
- $o_vmap allocate [self object] $argnum $type $val
- }
-
- method found_match {} {
- return $o_found_match
- }
- method matched_argument {} {
- return $o_matched_argument
- }
- method matched_argnum {} {
- return $o_matched_argnum
- }
- method set_matched_argument {argnum a} {
- #could be empty string
- if {$o_found_match} {
- error "[self object].set_matched_argument processor:$o_name already found match '$o_matched_argument' - cannot set again"
- }
- if {![my can_match $a]} {
- error "error [self class].set_matched_argument processor:$o_name cannot match '$a' (matchspec: $o_matchspec)"
- }
- set o_found_match 1
- set o_matched_argument $a
- set o_matched_argnum $argnum
- }
- method has_explicit_matchspec {} {
- return [dict exists $o_pinfo match]
- }
- method matchspec {} {
- return $o_matchspec
- }
- method can_match {a} {
- if {!$o_found_match} {
- foreach m $o_matchspec {
- if {[regexp -- $m $a]} {
- return 1
- }
- }
- return 0
- } else {
- return 0
- }
- }
- #??
- method can_allocate_flags {} {
- }
-
-
-
-
-
- #if we are a parent - this is own name
- method parentname {} {
- return $o_parentname
- }
- method parent {} {
- return [::flagfilter::obj::PARENTS_$o_runid item $o_parentname]
- }
- method is_parent {} {
- return [expr {!$o_is_sub}]
- }
- method children {} {
- if {!$o_is_sub} {
- return $o_col_children
- } else {
- #raise error?
- return ""
- }
- }
- method mashopts {} {
- return $o_mashopts
- }
- method singleopts {} {
- return $o_singleopts
- }
- method pairopts {} {
- return $o_pairopts
- }
- method longopts {} {
- return $o_longopts
- }
-
- #whether flag categorized as solo by this processor
- method arg_is_defined_solo_to_me {a} {
- if {(![string match "-*" $a]) && (![string match "/*" $a])} {
- #not even flaglike
- return 0
- }
- if {[my can_match $a]} {
- return 0
- }
- if {$a in {- --}} {
- #specials not defined as solos
- return 0
- }
-
- if {$o_name eq "global"} {
-
- } elseif {$o_name eq "tail_processor"} {
-
- }
-
- if {$a in $o_singleopts} {
- return 1
- }
- if {"any" in $o_singleopts} {
- return 1
- }
- set equalposn [string first "=" $a]
- if {$equalposn >=1} {
- if {"any" in $o_longopts} {
- return 1
- } else {
- set namepart [string range $a 0 $equalposn-1]
- foreach lo $o_longopts {
- if {[string match "${namepart}=*" $lo]} {
- return 1
- }
- }
- }
- }
- #Flag could still be part of a solo if it is in mashopts *and* has a value following it as part of the mash
- #- but if it's a pairopt, but not mashable - we can rule it out now
- if {($a in $o_pairopts) && ($a ni $o_mashopts)} {
- return 0
- }
- set flagletters [split [string range $a 1 end] ""]
- set posn 1
- #trailing letters may legitimately not be in mashopts if they are part of a mashed value
- #we can return 0 if we hit a non-mash flag first.. but at each mashflag we need to test if we can classify as definitely solo or not, or else keep processing
- foreach l $flagletters {
- if {"-$l" ni $o_mashopts} {
- #presumably an ordinary flag not-known to us
- return 0
- } else {
- if {"-$l" in $o_pairopts} {
- if {$posn == [llength $flagletters]} {
- #in pairopts and mash - but no value for it in the mash - thefore not a solo
- return 0
- } else {
- #entire tail is the value - this letter is effectively solo
- return 1
- }
- } elseif {"-$l" in $o_singleopts} {
- #not allowed to take a value - keep processing letters
- } else {
- #can take a value! but not if at very end of mash. Either way This is a solo
- return 1
- }
- }
- }
- #This object should not treat the flag as a known solo
- #- so if it is allowed to consume it, it may fall back on examining the subsequent argument's flaginess(?)
- return 0
- }
-
-
- method get_opts {} {
- return [list mashopts $o_mashopts singleopts $o_singleopts pairopts $o_pairopts longopts $o_longopts]
- }
- #include parent opts
- #we use the terminology 'option' for "-" prefixed items belonging to a -commandprocessors spec as opposed to more general -flags
- #Note - this may also be called on the default "tail_processor", which will return empty sets, or an overridden tail_processor which may have data
- method get_combined_opts {} {
- set objparent [::flagfilter::obj::PARENTS_$o_runid item $o_parentname]
- set parentopts [$objparent get_opts]
- set mashopts [dict get $parentopts mashopts]
- set singleopts [dict get $parentopts singleopts]
- set pairopts [dict get $parentopts pairopts]
- set longopts [dict get $parentopts longopts]
- if {[my is_sub]} {
- #this spec is a sub
- set subopts [my get_opts]
- #does order matter? could use struct::set union ?
- foreach m [dict get $subopts mashopts] {
- if {$m ni $mashopts} {
- lappend mashopts $m
- }
- }
- foreach s [dict get $subopts singleopts] {
- if {$s ni $singleopts} {
- lappend singleopts $s
- }
- }
- foreach po [dict get $subopts pairopts] {
- if {$po ni $pairopts} {
- lappend pairopts $po
- }
- }
- foreach lo [dict get $subopts longopts] {
- if {$lo ni $longopts} {
- lappend longopts $lo
- }
- }
-
- }
- return [list mashopts $mashopts singleopts $singleopts pairopts $pairopts longopts $longopts]
- }
-
- }
-
-
-
-
-
-
-
-
-
-
-
- proc get_command_info {cmdname cspecs} {
- foreach item $cspecs {
- lassign $item cmd specinfo
- if {$cmd eq $cmdname && [dict exists $specinfo dispatch]} {
- return $specinfo
- }
- }
- return [list]
- }
- #### check_flags
- # does not support unvalued flags - unless explicitly specified in -soloflags (global) or in -singleopts for a commandprocessor
- #e.g not supported: v1 v2 -arg1 arg1val -debug -anotherflag anotherflagval
- # - unless -soloflags is something like -soloflags {-debug} or -soloflags {{-debug 1}} where 1 is the default. In this case - we can no longer support accepting a value for -soloflags - the processor will not assign it an argument from the commandline.
- #e.g not supported (unless -debug in -soloflags): v1 v2 -arg1 arg1val -anotherflag anotherflagval -debug
- #e.g supported: v2 v2 -arg1 arg1val -debug 1 -anotherflag anotherflagval
- # supports positional arguments - but only if specified in -commandprocessors
- # todo
- # - supports -- for treating following arg as value even if it looks like a flag
- # - supports - for reading stdin
- # expects at least -values
- # other options -caller -defaults -required -extras -commandprocessors
- # -soloflags (these are flags that *must* be solo - ie they cannot take an argument ) if no default specified they are boolean defaulting to 1, repeated instances in -values will be appended to a list.
- # The only flag that can be a mix of solo or not, is the very last flag in the values list. In this case it must not be in the -soloflags list, but it will default to a boolean 1 to indicate presence.
- proc check_flags {args} {
- set runid [flagfilter::get_new_runid]
- ####################################################
- #puts "Entered checkflags, args $args"
- set distanceToTop [info level]
- set callerlist [list]
- set was_dispatched_by_another 0 ;#used to
- for {set i 1} {$i < $distanceToTop} {incr i} {
- set callerlevel [expr {$distanceToTop - $i}]
- set callerinfo [info level $callerlevel]
- set firstword [lindex $callerinfo 0]
- if {[string match "*check_flags*" $firstword]} {
- set was_dispatched_by_another 1
- }
- lappend callerlist $firstword
- }
- #puts stdout "callerlist: $callerlist"
-
- #first handle args for check_flags itself
- if {[catch {lindex [info level -1] 0} caller]} {
- set caller ""
- }
- #puts stderr ">>>>check_flags caller $caller"
- get_one_paired_flag_value {-x 1} -x ;#
-
- #manually check for -caller even if unbalanced args
- #we only need to use get_one_paired_flag_value because we haven't yet checked args is a properly formed paired list and if -caller is present we want to use it for clearer error messages.
- #use normal dict operations to retrieve other flags.
- #if failed to retrieve.. fall through to checks below
- if {![catch {get_one_paired_flag_value $args -caller} flag_value_result]} {
- set caller $flag_value_result
- }
- #puts stderr ">>>>check_flags caller $caller"
-
-
-
-
- set cf_defaults [dict create\
- -caller $caller\
- -return [list arglistremaining]\
- -match [list]\
- -commandprocessors [list]\
- -soloflags [list]\
- -extras [list]\
- -defaults [list]\
- -required [list]\
- -values \uFFFF\
- -debugargs 0\
- ]
- dict set cf_defaults -debugargsonerror 1 ;#error level to use when dispatch error occurs.. will not set lower than -debugargs
-
-
-
- if {([llength $args] % 2) != 0} {
- do_error "check_flags error when called from '$caller' :check_flags must be called with even number of arguments of form: -flag value Valid flags are: '[dict keys $cf_defaults]' \n got: $args"
- }
- set cf_args $cf_defaults
- foreach {k v} $args {
- switch -- $k {
- -caller - -return - -match - -commandprocessors - -soloflags - -extras - -defaults - -required - -values - -debugargs - -debugargsonerror {
- dict set cf_args $k $v
- }
- default {
- do_error "check_flags error when called from ${caller}: Unknown option '$k': must be one of '[dict keys $cf_defaults]' \nIf calling check_flags directly, put args being checked in -values {...}"
- }
- }
- }
- unset args
- ####################################################
- #now look at -values etc that check_flags is checking
-
- set caller [dict get $cf_args -caller]
-
- set debugargs [dict get $cf_args -debugargs]
- dict set debugc -debugargs [dict get $cf_args -debugargs]
- dict set debugc -source "check_flags $caller"
- do_debug 1 $debugc "DEBUG-START $caller"
-
- set returnkey [dict get $cf_args -return]
- set defaults [dict get $cf_args -defaults]
- if {([llength $defaults] % 2) != 0} {
- do_error "check_flags error when called from '$caller' :-defaults must be a list containing an even number of arguments of form: -flag value'"
- }
- set required [dict get $cf_args -required]
-
-
- set acceptextra [dict get $cf_args -extras]
-
- set supplied [string trim [dict get $cf_args -values]]
- set soloflags [dict get $cf_args -soloflags] ;#By their nature - solo flags are unlikely to be automatically 'required' - review
- set solos_with_defaults [list]
- foreach solo_spec $soloflags {
- if {[llength $solo_spec] == 1} {
- lappend solos_with_defaults $solo_spec 1
- } else {
- lappend solos_with_defaults [lindex $solo_spec 0] [lindex $solo_spec 1]
- }
-
- }
-
- if {$debugargs >= 3} {
- set prefix "| $caller>"
- puts -nonewline stderr "$prefix [string repeat - 30]\n"
- puts -nonewline stderr "$prefix input\n"
- puts -nonewline stderr "$prefix [string repeat - 30]\n"
- #puts stderr "$caller $cf_args"
- dict for {k v} $cf_args {
- if {$k ne "-commandprocessors"} {
- puts -nonewline stderr "$prefix \[$k\]\n"
- puts -nonewline stderr "$prefix $v\n"
- }
- }
- if {$debugargs >=4} {
- puts -nonewline stderr "$prefix \[-commandprocessors\]\n"
- foreach record [dict get $cf_args -commandprocessors] {
- puts -nonewline stderr "$prefix $record\n"
- }
- }
- puts -nonewline stderr "$prefix [string repeat - 30]\n"
- #dict for {key val} $cf_args {
- # puts stderr " $key"
- # puts stderr " $val"
- #}
- }
-
-
- ##################################################################################################
- # allocate_arguments does the main work of processing non-flagged items in the main supplied argument list into flagged versions depending on the specs in -commandprocessors
- # It sets defaults only for those arguments processed by a '-commandprocessors' spec.
- # We must supply it with the -soloflags info because the solo flags affect what is considered an operand.
- set command_specs [dict get $cf_args -commandprocessors] ;#may be empty list - that's ok - it will still populate the 'flagged' and 'arglist' return-dict members.
-
- #some of these are keys returned by allocate_arguments
- # - some (e.g supplied) are added by check_flags
- # This list is the list of -return values that can be used with check_args
- set flaginfo_returns [list \
- parseerrors \
- parsestatus \
- flagged \
- flaggedremaining \
- flaggednew \
- unflagged \
- unflaggedremaining \
- unflaggedlistremaining \
- listremaining \
- arglist \
- arglistremaining \
- impliedunflagged \
- impliedflagged \
- classifications \
- gridstring \
- ranges \
- dispatch \
- dispatchstatuslist \
- dispatchresultlist \
- dispatchstatus \
- supplied \
- defaults \
- status \
- vmapobject \
- ]
-
- set PROCESSORS [col_allprocessors create ::flagfilter::obj::PROCESSORS_$runid]
- set PARENTS [col_parents create ::flagfilter::obj::PARENTS_$runid]
-
- #
- #set command_specs [concat [list {global {}}] $command_specs]
- lappend command_specs {tail_processor {}}
-
- foreach cspec $command_specs {
- set obj [cprocessor new $cspec $runid] ;#runid gives access to the context-objects PROCESSORS_runid & PARENTS_runid
- if {[$obj is_parent]} {
- $PARENTS add_parent $obj
- }
- #do_debug 1 $debugc "CONFIGURING OBJECT for commandprocessor [$obj name]"
- $PROCESSORS add_processor $obj
- }
- do_debug 1 $debugc "ADDED [$PROCESSORS count] processors to main commandprocessor collection"
- do_debug 1 $debugc "ADDED [$PARENTS count] processors to the parents collection"
- $PROCESSORS set_commandspecs $command_specs
-
- #allocate_arguments uses the PROCESSORS object
- set processed_arguments [allocate_arguments $PROCESSORS $solos_with_defaults $supplied $cf_args $caller]
- #set processed_arguments [allocate_arguments {} $supplied]
-
- set newly_flagged_positionals [dict get $processed_arguments flaggednew]
- set unflaggedremaining [dict get $processed_arguments unflaggedremaining]
- set unflaggedlistremaining [dict get $processed_arguments unflaggedlistremaining]
- set dispatch [dict get $processed_arguments dispatch]
- set flaggedremaining [dict get $processed_arguments flaggedremaining]
- set RETURNED_VMAP [dict get $processed_arguments vmapobject]
-
-
-
- if {$debugargs >= 3} {
- set prefix "| $caller>"
- puts -nonewline stderr "$prefix [string repeat - 30]\n"
- puts -nonewline stderr "$prefix output\n"
- puts -nonewline stderr "$prefix [string repeat - 30]\n"
- #puts stderr "processed_arguments: $processed_arguments"
- dict for {key val} $processed_arguments {
- puts -nonewline stderr "$prefix $key\n"
- puts -nonewline stderr "$prefix $val\n"
- }
- puts -nonewline stderr "$prefix [string repeat - 30]\n"
- }
-
- ##################################################################################################
-
-
-
-
-
- if {![llength $newly_flagged_positionals]} {
- if {($supplied eq "\uFFFF") || ![llength $supplied]} {
- #do_error "check_flags error when called from ${caller}: missing or empty -values"
- }
- }
-
- #probably not something to enforce... we might pass on unbalanced lists to other check_args etc.
- #if {([llength $supplied] % 2) != 0} {
- # do_error "${caller}: Error. $caller must be called with even number of arguments of form: -flag value Valid flags are: '[dict keys $defaults]'\n received values: $supplied"
- #}
-
-
-
- set new_arg_list [dict get $processed_arguments arglistremaining]
- set flagged_list [dict get $processed_arguments flagged]
- #set suppliedkeys_with_extrakeys [concat [dict keys $supplied] [dict keys $newly_flagged_positionals]]
- #puts stdout "suppliedkeys and new keys: $suppliedkeys_with_extrakeys"
-
- #todo - add flaggednew to required if all was specified?
- #check invalid flags if not indicated in -extras , either explicitly or with 'extra'
- set flags_from_required [get_flagged_only $required {}]
- #set known_flags [lsort -unique -nocase [concat [dict keys $defaults] $flags_from_required $soloflags]] ;#why -nocase? why should -l and -L collapse to the uppercase version?
- set known_flags [punk::lib::lunique_unordered [concat [dict keys $defaults] $flags_from_required $soloflags ]]
- foreach spec $command_specs {
- lassign $spec parentname pinfo
- if {[string match -* $parentname] && $parentname ni $known_flags} {
- lappend known_flags $parentname
- }
- if {[dict exists $pinfo sub]} {
- if {[string match -* [dict get $pinfo sub]]} {
- lappend known_flags [dict get $pinfo sub]
- }
- }
- }
- do_debug 2 $debugc "------------------->known_flags: $known_flags soloflags:$soloflags"
- set invalid_flags [list]
- if {"all" ni [string tolower $acceptextra]} {
- if {"none" in [string tolower $acceptextra]} {
- set ok_extras [list]
- } elseif {[llength $acceptextra]} {
- set ok_extras $acceptextra
- }
- #todo
- #puts stderr " check_flags - temporary disable of checking for invalid flags"
- set pairflagged $flagged_list
- foreach {f v} $pairflagged {
- if {$f ni $acceptextra && $f ni $known_flags} {
- lappend invalid_flags $f
- }
- }
- }
- if {[llength $invalid_flags]} {
- do_error "check_flags $caller error when called from ${caller}: unknown flags '$invalid_flags'"
- }
-
- set calc_required [list]
- set keywords_in_required [lsearch -inline -all -not $required -*]
- set bad_keywords_in_required [lsearch -regexp -nocase -all -inline -not $keywords_in_required "all|none"]
- if {[llength $bad_keywords_in_required]} {
- do_error "check_flags error when called from ${caller}: bad flags in '-required' it must be a list of flags of the form -flagname or ONLY one of the keywords 'none' or 'all'"
- }
- #keywords_in_required now known to be only comprised of (possibly case variant) values of all|none
- if {[llength $keywords_in_required] > 1} {
- do_error "check_flags error when called from ${caller}: specifying both 'none' and 'all' in -required is not valid, and repeated values are not valid."
- }
- if {"none" eq [string tolower [lindex $keywords_in_required 0]]} {
- set calc_required [list]
- }
- set flags [lsearch -inline -all $required -*]
-
- if {[llength $required]} {
- if {[lsearch -nocase $keywords_in_required "all"] >= 0} {
- #'all' can be present with other flags - and indicates we also require all the flags from -defaults
- dict for {k -} $defaults {
- if {$k ni $calc_required} {
- lappend calc_required $k
- }
- }
- }
- }
-
- set classifications [dict get $processed_arguments classifications] ;#assertion - ordered by numerically increasing key representing positions in supplied argument list
- set rangesets [$RETURNED_VMAP get_ranges_from_classifications $classifications]
- set ranges [dict get $rangesets -ranges]
- set rangesbytype [dict get $rangesets -rangesbytype] ;#unallocated are split into flag,operand and endofoptions - further splitting is easy enough to do by looking up the classifications list for each position in the supplied arg list.
- #tailflags are the same for all dispatch items
- set tailflagspaired [tailflagspaired $defaults $supplied $classifications $rangesbytype]
-
-
- set dict_supplied [dict create supplied $supplied]
- set dict_defaults [dict create defaults $defaults]
- set dict_ranges [dict create ranges $ranges]
- set dict_rangesbytype [dict create rangesbytype $rangesbytype]
- set raise_dispatch_error_instead_of_return ""
- set dict_dispatch_results [list dispatchstatuslist [list] dispatchresultlist [list] dispatchstatus "ok"]
- #todo - only dispatch if no unallocated args (must get tail_processor to allocate known flags to 'global')
- if {[llength $dispatch]} {
- set dispatchstatuslist [list]
- set dispatchresultlist [list]
- set dispatchstatus "ok"
- #each dispatch entry is a commandname and dict
- #set dispatchrecord [lrange $dispatch 0 1]
- set re_argnum {%arg([0-9^%]+)%}
- set re_argtake {%argtake([0-9^%]+)%}
- set re_dquotedparts {(?:(?:\"[^\"]*\")|(?:\"[^\"]*"))|(?:\S*[^ \"])} ;#for use with regexp -all -inline
- #e.g {"a b" 'b x' "x cd "e f" g a} -> {"a b"} 'b x' {"x cd "} e f {" g a}
- #dumb-editor rebalancing quote for above comment "
- foreach {parentname dispatchrecord} $dispatch {
- set commandinfo [get_command_info $parentname $command_specs]
-
- do_debug 1 $debugc ">>>>>DISPATCHRECORD: $dispatchrecord"
-
- # e.g lscmd lscmd natsortcommandline_ls lscmd.dir x
-
- do_debug 2 $debugc "commandinfo for $parentname: $commandinfo"
- set command [dict get $dispatchrecord command]
- #support for %x% placeholders in dispatchrecord command
- set command [string map {%match% %matched%} $command] ;#alias
- set command [string map [list %matched% [dict get $dispatchrecord matched]] $command]
-
- set argnum_indices [regexp -indices -all -inline $re_argnum $command]
- if {[llength $argnum_indices]} {
- foreach {argx_indices x_indices} $argnum_indices {
- #argx eg %arg12%
- set argx [string range $command {*}$argx_indices]
- set x [string range $command {*}$x_indices]
- set command [string map [list $argx [lindex [dict get $dispatchrecord arguments] $x]] $command]
- }
- }
-
- set argsreduced [dict get $dispatchrecord arguments]
- #set rawparts [regexp -all -inline $re_dquotedparts [dict get $dispatchrecord raw]]
-
- #review!
- #how will this behave differently on unix
- package require punk::winrun
- set rawparts [punk::winrun::unquote_wintcl [dict get $dispatchrecord raw]]
- #set argtake_indices [regexp -indices -all -inline $re_argtake $command]
-
-
- set start 0
- while {[regexp -start $start -indices $re_argtake $command argx_indices x_indices]} {
- #argx eg %argtake12%
- set argx [string range $command {*}$argx_indices]
- set x [string range $command {*}$x_indices]
- set argval [lindex [dict get $dispatchrecord arguments] $x]
- set replacementlen [string length $argval]
- set command [string map [list $argx $argval] $command]
- set start [expr {[lindex $argx_indices 0] + $replacementlen}]
- set argsreduced [lremove $argsreduced $x]
- set rawparts [lremove $rawparts $x]
- }
- dict set dispatchrecord arguments $argsreduced
- if {$start > 0} {
- set rawreduced [join $rawparts]
- dict set dispatchrecord raw $rawreduced
- }
-
- set argvals [dict get $dispatchrecord arguments]
- set matched_operands [list]
- set matched_opts [list]
- set matched_in_order [list]
- set prefix "${parentname}."
- set prefixlen [string length $prefix]
- foreach {k v} $argvals {
- #puts "$$$$ $k"
- if {[string equal -length $prefixlen $prefix $k]} {
- #key is prefixed with "commandname."
- set k [string replace $k 0 $prefixlen-1]
- }
- #todo - -- ?
- if {[string match -* $k]} {
- lappend matched_opts $k $v
- lappend matched_in_order $k $v
- } else {
- set kparts [split $k .]
- lappend matched_operands $v
- lappend matched_in_order $v
- }
- }
-
- if {![dict exists $commandinfo dispatchtype]} {
- set dispatchtype tcl
- } else {
- set dispatchtype [dict get $commandinfo dispatchtype]
- }
- if {![dict exists $commandinfo dispatchglobal]} {
- if {$dispatchtype eq "tcl"} {
- set dispatchglobal 1
- } else {
- set dispatchglobal 0
- }
- } else {
- set dispatchglobal [dict get $commandinfo dispatchglobal]
- }
- #generally we only want to dispatch remaining flagged, and only at the tail end.(as opposed to flags occurring between command groups)
- # -It doesn't usually make much sense to dispatch remaining unflagged items, and it would be rare to require flags occurring before the command.
- #however - there are potential commands such as help, dryrun or maybe an analysis command that may need to see unconsumed operands or even look 'back' at prior items
- ##update 2023-03 - we definitely want to look back to prior non-matches when we match on a script e.g tclsh8.6 -someflag etc xxx.tcl scriptarg1 -etc
- # if we match and dispatch on *.tcl - then we may need 'tclsh8.6 -someflag etc' as the interpreter (possibly with arguments) to use.
- # we may need a 'script' dispatchtype (as well as the option to just pass these prior arguments as additional options for some other dispatchtypes)
- #
- # todo - add supported dispatchglobal values such as all, pre, post, allpre, allpost, and classifications
- # where pre & post are only those occurring directly before and after the command and its args, i.e not extending beyond any prior or subsequent other command.
- # classifications would be flagged as -classifications $classifications whereas pre and post would be added directly if specified singly, or flagged with -pre, -post etc if multiple are specified
- # Those beginning with 'all' should also be wrapped in flags, because potentially they come from disjointed sections of the argumentlist
- # - and we generally shouldn't supply arguments next to each other that weren't contiguous in the original list
- # The 1,true,yes,tailflagspaired value is designed for the usecase where a common set of tail flags e.g -debug can apply to any commands matched by the filter.
- # tail = all unallocated args after final command, including operands and end-of-options '--' (todo)
- # tailflags = all unallocated *contiguous* flags after the final command and final operands. (ie it will deliberately miss flags following last command if there is a later operand) (todo)
- # tailflagspaired = same as tailflags, but any solo-flags are defaulted to 1 (flags not merged, so there might be duplicate keys) so that it's a fully paired list
- # In other situations - post may make sense to get the very next set of unconsumed arguments.
- if {[string tolower $dispatchglobal] in [list 1 true yes tailflagspaired]} {
- set command_range_posn [lsearch -index 1 $ranges $parentname]
- set extraflags $tailflagspaired
- } else {
- set extraflags [list]
- }
-
- #jn concat allows $command to itself be a list
- ##tcl dispatchtype
- dict set dispatchrecord dispatchtype $dispatchtype
- switch -- $dispatchtype {
- tcl {
- do_debug 1 $debugc "DISPATCHING with tcl arg order: $command $matched_operands $matched_opts $extraflags"
- #set commandline [list $command {*}$matched_operands {*}$matched_opts {*}$extraflags]
- set commandline [concat $command $matched_operands $matched_opts $extraflags]
- }
- raw {
- do_debug 1 $debugc "DISPATCHING with raw args : $command [dict get $dispatchrecord raw]"
- #set commandline [list $command {*}[dict get $dispatchrecord raw] {*}$extraflags]
- set commandline [concat $command [dict get $dispatchrecord raw] $extraflags]
- }
- shell {
- do_debug 1 $debugc "DISPATCHING with shell args : $command [dict get $dispatchrecord raw]"
- #assume the shell arguments are in one quoted string?
- set commandline [concat $command [list [dict get $dispatchrecord raw]] $extraflags]
- }
- default {
- #non quoted shell? raw + defaults?
- do_debug 1 $debugc "DISPATCHING with given arg order: $command $matched_in_order $extraflags"
- #set commandline [list $command {*}$matched_in_order {*}$extraflags]
- set commandline [concat $command $matched_in_order $extraflags]
- }
- }
-
- dict set dispatchrecord asdispatched $commandline
- set dispatchresult ""
- set dispatcherror ""
- if {![catch {{*}$commandline} cmdresult]} {
- set dispatchresult $cmdresult
- lappend dispatchstatuslist [list status ok cmd $parentname outputlength [string length $cmdresult]]
- lappend dispatchresultlist $cmdresult
- } else {
- set dispatchstatus "error"
- set dispatcherror $cmdresult
- #don't add to dispatchresultlist
- lappend dispatchstatuslist [list status err cmd $parentname outputlength 0 error $cmdresult]
- if {!$was_dispatched_by_another} {
- #this is the first (or a direct) call to check_flags - so make sure error gets raised in this proc rather than just storing the error in the data and returning
- set raise_dispatch_error_instead_of_return "dispatchstatuslist:\n[join $dispatchstatuslist \n] \nerrinfo:\n $::errorInfo"
- dict set dispatchrecord result $dispatchresult
- dict set dispatchrecord error $dispatcherror
- dict set dispatch $parentname $dispatchrecord
-
- break
- #return -code error "check_flags error during command dispatch:\n$cmdresult"
- }
- #we've been dispatched from another check_flags - so ok to propagate the error up via the dispatchrecord/dispatchstatuslist
- }
- dict set dispatchrecord result $dispatchresult
- dict set dispatchrecord error $dispatcherror
- dict set dispatch $parentname $dispatchrecord
- }
-
- set dict_dispatch_results [list dispatchcaller $caller dispatchstatuslist $dispatchstatuslist dispatchresultlist $dispatchresultlist dispatchstatus $dispatchstatus]
- }
- #end llength $dispatch
-
-
- set combined [dict merge $dict_defaults $dict_supplied $processed_arguments $dict_ranges $dict_rangesbytype $dict_dispatch_results]
- dict set combined dispatch $dispatch ;#update with asdispatched info
- if {([dict get $combined parsestatus] eq "ok") && ([dict get $combined dispatchstatus] eq "ok")} {
- dict set combined status "ok"
- } else {
- dict set combined status "error"
- }
- do_debug 1 $debugc "COMBINED:$combined"
-
-
- set returnkey [string tolower $returnkey]
- if {"all" in $returnkey} {
- set returnval $combined
- #set returnval [dict merge $combined $dict_dispatch_results]
- } else {
- if {[llength $returnkey] == 1} {
- set invalid 0
- #todo - support multiple merge?
- set right ""
- if {[regexp -all {\|} $returnkey] == 1} {
- lassign [split $returnkey |] left right
- set joinparts [split $left ,]
- } else {
- set joinparts [split $returnkey ,]
- }
- foreach j [concat $joinparts $right] {
- if {$j ni $flaginfo_returns} {
- set invalid 1
- }
- }
- set returnval [list]
- if {!$invalid} {
- foreach j $joinparts {
- lappend returnval {*}[dict get $combined $j]
- }
- if {[string length $right]} {
- set returnval [dict merge $returnval $defaults $returnval]
- }
- } else {
- set returnval [list callerrors [list "-return '$returnkey' not valid"]]
- }
- } else {
- set callerrors [list]
- set returnval [dict create]
- foreach rk $returnkey {
- if {$returnkey in $flaginfo_returns} {
- dict set returnval $rk [dict get $combined $returnkey]
- } else {
- lappend callerrors [list "-return '$returnkey' not valid"]
- }
- }
- if {[llength $callerrors]} {
- dict set returnval callerrors $callerrors
- }
- }
- }
-
- do_debug 1 $debugc "[string repeat = 40]"
- do_debug 1 $debugc "dispatch_results: $dict_dispatch_results"
- do_debug 1 $debugc "[string repeat - 40]"
-
- if {[string length $raise_dispatch_error_instead_of_return]} {
- set errdebug [dict get $cf_args -debugargsonerror]
- if {$errdebug > [dict get $cf_args -debugargs]} {
- dict set debugc -debugargs $errdebug
- }
- }
-
- set debuglevel_return 2
- set debugdict [concat {*}[lmap k [dict keys $combined] {list $k $debuglevel_return}]] ;#create a dict of keys from combined, all defaulted to $debuglevel_return
- if {[llength [dict get $combined parseerrors]]} {
- dict set debugdict "parseerrors" 0
- } else {
- dict set debugdict "parseerrors" 2
- }
- dict set debugdict "defaults" 1
- dict set debugdict "supplied" 1
- dict set debugdict "dispatch" 1
- dict set debugdict "ranges" 1
- dict set debugdict "rangesbytype" 1
- dict set debugdict "dispatchstatus" 1
- if {[dict get $combined "status"] eq "ok"} {
- dict set debugdict "status" 1
- } else {
- dict set debugdict "status" 0
- }
-
- do_debug 1 $debugc "returning '$returnkey'"
- do_debug 1 $debugc "returnval '$returnval'"
- if {([llength $returnval] % 2) == 0} {
- do_debug 1 $debugc "returnkeys '[dict keys $returnval]'"
- }
- do_debug 1 $debugc "[string repeat = 40]"
- dict for {k v} $combined {
- set dlev [dict get $debugdict $k]
- switch -- $k {
- dispatch {
- set col1 [string repeat " " 12]
- #process as paired list rather than dict (support repeated commands)
- set i 0
- foreach {cmdname cmdinfo} $v {
- set field1 [string repeat " " [expr {[string length $cmdname]}]]
- set col2_dispatch [string repeat " " [expr {[string length $cmdname] + 15}]]
- set j 0
- foreach {ckey cval} $cmdinfo {
-
- if {$i == 0 && $j == 0} {
- set c1 [overtype::left $col1 "dispatch"]
- } else {
- set c1 [overtype::left $col1 { ... }]
- }
-
- if {$j == 0} {
- set f1 [overtype::left $field1 $cmdname]
- set c2 [overtype::left $col2_dispatch "$f1 $ckey"]
- } else {
- set f1 [overtype::left $field1 ...]
- set c2 [overtype::left $col2_dispatch "$f1 $ckey"]
- }
- #leave at debug level 1 - because dispatch is generally important
- do_debug $dlev $debugc "${c1}${c2} $cval"
-
- incr j
- }
- incr i
- }
-
- #do_debug 1 $debugc "[overtype::left $col1 $k] [lindex $v 0] [list [lindex $v 1]]"
- #foreach {nm rem} [lrange $v 2 end] {
- # do_debug 1 $debugc "[overtype::left $col1 { ... }] $nm [list $rem]"
- #}
- }
- dispatchresultlist {
- set col1 [string repeat " " 25]
- set i 0
- foreach dresult $v {
- if {$i == 0} {
- set c1 [overtype::left $col1 $k]
- } else {
- set c1 [overtype::left $col1 { ... }]
- }
- do_debug $dlev $debugc "$c1 $dresult"
- incr i
- }
- }
- classifications {
- set col1 [string repeat " " 25]
- set len [dict size $v]
- if {$len == 0} {
- do_debug $dlev $debugc "[overtype::left $col1 $k]"
- continue
- }
- set max [expr {$len -1}]
- set numlines [expr $len / 3 + 1]
- if {($len % 3) == 0} {
- incr numlines -1
- }
- set j 0
- for {set ln 0} {$ln < $numlines} {incr ln} {
- if {$ln == 0} {
- set c1 "[overtype::left $col1 $k]"
- } else {
- set c1 "[overtype::left $col1 { ... }]"
- }
- set line ""
- for {set col 0} {$col < 3} {incr col} {
- if {$j <= $max} {
- append line "$j [list [dict get $v $j]] "
- }
- incr j
- }
- do_debug $dlev $debugc "$c1 [string trim $line]"
- }
- }
- gridstring {
- set col1 [string repeat " " 25]
- set i 0
- foreach ln [split $v \n] {
- if {$i == 0} {
- set c1 [overtype::left $col1 $k]
- } else {
- set c1 [overtype::left $col1 { ... }]
- }
- do_debug $dlev $debugc "$c1 $ln"
- incr i
- }
- }
- default {
- set col1 [string repeat " " 25]
- do_debug $dlev $debugc "[overtype::left $col1 $k] $v"
- }
- }
- }
-
-
- # ---------------------------------
- foreach obj [$PARENTS items] {
- catch {$obj destroy}
- }
- $PARENTS destroy
- #puts "PROCESSORS: $PROCESSORS"
- foreach obj [$PROCESSORS items] {
- catch {$obj destroy}
- }
- $PROCESSORS destroy
- catch {$RETURNED_VMAP destroy}
- # ---------------------------------
-
- do_debug 1 $debugc "[string repeat = 40]"
- do_debug 1 $debugc "DEBUG-END $caller"
- if {[string length $raise_dispatch_error_instead_of_return]} {
- return -code error $raise_dispatch_error_instead_of_return
- }
-
-
- return $returnval
- }
-
- proc tailflagspaired {defaults supplied classifications rangesbytype} {
- lassign [lindex $rangesbytype end] c tp a b
- if {($c eq "unallocated") && ($tp eq "flagtype")} {
- set tail_unallocated [lrange $supplied $a $b]
- } else {
- set tail_unallocated [list]
- }
- #set extraflags [list]
- set extraflags [punk::lib::dict_merge_ordered $defaults $tail_unallocated]
- #dict merge based operation can't work if there are solo_flags?
- #review
- if {[llength $tail_unallocated]} {
- for {set i $a} {$i <=$b} {incr i} {
- set arginfo [dict get $classifications $i]
- lassign $arginfo class ftype v
- switch -- $ftype {
- flag - flagvalue {
- lappend extraflags $v
- }
- soloflag {
- lappend extraflags $v
- if {[dict exists $defaults $v]} {
- lappend extraflags [dict get $defaults $v]
- } else {
- lappend extraflags 1
- }
- }
- }
- }
- foreach {k v} [dict get $defaults] {
- if {$k ni $extraflags} {
- lappend extraflags $k $v
- }
- }
- } else {
- set extraflags $defaults
- }
- return $extraflags
- }
-
- proc tailflagspaired1 {defaults supplied classifications rangesbytype} {
- lassign [lindex $rangesbytype end] c tp a b
- if {($c eq "unallocated") && ($tp eq "flagtype")} {
- set tail_unallocated [lrange $supplied $a $b]
- } else {
- set tail_unallocated [list]
- }
- #set all_post_unallocated_ranges [lsearch -all -inline -index 0 [lrange $rangesbytype $command_range_posn end] "unallocated"]
-
- set extraflags [list]
-
- #set extraflags [punk::lib::dict_merge_ordered $defaults $tail_unallocated]
- #dict merge based operation can't work if there are solo_flags with no value set
- if {[llength $tail_unallocated]} {
- for {set i $a} {$i <=$b} {incr i} {
- set arginfo [dict get $classifications $i]
- lassign $arginfo class ftype v
- switch -- $ftype {
- flag - flagvalue {
- lappend extraflags $v
- }
- soloflag {
- lappend extraflags $v
- if {[dict exists $defaults $v]} {
- lappend extraflags [dict get $defaults $v]
- } else {
- lappend extraflags 1
- }
- }
- }
- }
- foreach {k v} [dict get $defaults] {
- if {$k ni $extraflags} {
- lappend extraflags $k $v
- }
- }
- } else {
- set extraflags $defaults
- }
-
- }
-
-
-
-}
-
-
-namespace eval flagfilter {
-
- #punk::lib::dict_merge_ordered
-
-
-
- #retrieve *only* names that are dependant on the provided namekey - not the key itself
- # (query is sorted by the trailing numerical index which represents order the arguments were processed)
- proc flag_array_get_sorted_subs {arrname sep namekey} {
- upvar $arrname arr
- set allsubs [array names arr ${namekey}.*${sep}name,*]
- set rnames [lmap nm $allsubs {string reverse $nm}]
- set sorted_rnames [lsort -dictionary $rnames]
- set ordered [lmap nm $sorted_rnames {string reverse $nm}]
- return $ordered
- }
-
- proc flag_array_get_sorted_siblings {arrname sep namekey} {
- #determine parent by looking at dot - but confirm parent name is in array.
-
- }
-
-
-
- #dictionary based lsort of reversed names which are presumed to have a trailing separator of some sort and a number e.g: name,0 name,1 ... name,10 etc.
- #use -dictionary to ensure embedded numbers are sorted as integers
- proc array_names_sorted_by_tail {arrname nameglob} {
- upvar $arrname arr
- set matched_names [array names arr $nameglob]
- set rnames [lmap nm $matched_names {string reverse $nm}]
- set sorted_rnames [lsort -dictionary $rnames]
- return [lmap nm $sorted_rnames {string reverse $nm}]
- }
-
-
-}
-
-
-
-
-
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/funcl-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/funcl-0.1.tm
deleted file mode 100644
index e8430fb0..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/funcl-0.1.tm
+++ /dev/null
@@ -1,325 +0,0 @@
-package provide funcl [namespace eval funcl {
- variable version
- set version 0.1
-}]
-#funcl = function list (nested call structure)
-#
-#a basic functional composition o combinator
-#o(f,g)(x) == f(g(x))
-
-namespace eval funcl {
-
- #from punk::pipe
- proc arg_is_script_shaped {arg} {
- if {[string first " " $arg] >= 0} {
- return 1
- } elseif {[string first \n $arg] >= 0} {
- return 1
- } elseif {[string first ";" $arg] >= 0} {
- return 1
- } elseif {[string first \t $arg] >= 0} {
- return 1
- } else {
- return 0
- }
- }
-
-
- proc o args {
- set closing [string repeat {]} [expr [llength $args]-1]]
- set body "[join $args { [}] \$data $closing"
- return $body
- }
-
- proc o_ args {
- set body ""
- set tails [lrepeat [llength $args] ""]
- puts stdout "tails: $tails"
-
- set end [lindex $args end]
- if {[llength $end] == 1 && [arg_is_script_shaped $end]} {
- set endfunc [string map " $end" {uplevel 1 [list if 1 ]}]
- } else {
- set endfunc $end
- }
- if {[llength $args] == 1} {
- return $endfunc
- }
-
- set wrap { [}
- append wrap $endfunc
- append wrap { ]}
-
- set i 0
- foreach cmdlist [lrange $args 0 end-1] {
- set is_script 0
- if {([llength $cmdlist] == 1) && [arg_is_script_shaped [lindex $cmdlist 0]]} {
- set is_script 1
- set script [lindex $cmdlist 0]
- }
- set t ""
- if {$i > 0} {
- append body { [}
- }
- set posn [lsearch $cmdlist _]
- if {$posn <= 0} {
- append body $cmdlist
- if {$i == ([llength $args]-2)} {
- append body " $wrap"
- }
- #if {$i == [expr {[llength $args] -2}]} {
- # #append body " \$data"
- # append body " $wrap"
- #}
- if {$i > 0} {
- set t {]}
- }
- } else {
- append body [lrange $cmdlist 0 $posn-1]
- if {$i == ([llength $args] -2)} {
- #append body " \$data"
- append body " $wrap"
- }
- set t [lrange $cmdlist $posn+1 end]
- if {$i > 0} {
- append t { ]}
- }
- }
- lset tails $i $t
- incr i
- }
- append body [join [lreverse $tails] " "]
- puts stdout "tails: $tails"
-
- return $body
- }
-
- #review - consider _call -- if count > 1 then they must all be callable cmdlists(?)
- # what does it mean to have additional _fn wrapper with no other elements? (no actual function)
- #e.g _fn 2 5 6 somefunc {_fn 1 3 {_call 1 3 xxx}} {_fn 1 4 command {_fn ...}}
- # what type indicates running subtrees in parallel vs sequentially?
- # any reason to have _call count other than 1? Presumably the parent node indicates the parallelism/sequentialism etc.
- #
- #
- # accept or return a funcl (or funcltree if multiple funcls in one commandlist)
- # also accept/return a call - return empty list if passed a call
- proc next_funcl {funcl_or_tree} {
- if {[lindex $funcl_or_tree 0] eq "_call"} {
- return [list]
- }
- if {[lindex $funcl_or_tree 0] in [list "_fn" "_call"]} {
- set funcl $funcl_or_tree
- } else {
- error "funcltree not implemented"
- }
-
-
- set count [lindex $funcl 1]
- if {$count == 0} {
- #null funcl.. what is it? metadata/placeholder?
- return $funcl
- }
- set indices [lrange $funcl 2 [expr {1 + $count}]]
- set i 0
- foreach idx $indices {
- if {$i > 0} {
- #todo - return a funcltree
- error "multi funcl not implemented"
- }
- set next [lindex $funcl $idx]
- incr i
- }
-
- return $next
-
- }
-
- #convert a funcl to a tcl script
- proc funcl_script {funcl} {
- if {![llength $funcl]} {
- return ""
- }
- set body ""
- set tails [list]
-
- set type [lindex $funcl 0]
- if {$type ni [list "_fn" "_call"]} {
- #todo - handle funcltree
- error "type $type not implemented"
- }
-
-
- #only count of 1 with index 3 supported(?)
- if {$type eq "_call"} {
- #leaf
- set cmdlist [lindex $funcl 3]
- return $cmdlist
- }
-
- #we will use next_funcl to walk the nodes.. todo support treefuncl response from next_funcl which could branch multiple times.
- #by continually passing back the resulting treefuncl/funcl to next_funcl we can process in correct order (?)
- # we would still need to maintain state to stitch it back together once returned from a subtree..
- # ie multiple tail parts
- set count [lindex $funcl 1]
-
- if {$count == 1} {
- set idx [lindex $funcl 2]
- if {$idx == 3} {
- set cmdlist_pre [list]
- } else {
- set cmdlist_pre [lrange $funcl 3 $idx-1]
- }
- append body $cmdlist_pre
- set t [lrange $funcl $idx+1 end]
- lappend tails $t
- } else {
- #??
- error "funcl_script branching not yet supported"
- }
-
-
- set get_next 1
- set i 1
- while {$get_next} {
- set funcl [next_funcl $funcl]
- if {![llength $funcl]} {
- set get_next 0
- }
- lassign $funcl type count idx ;#todo support count > 1
- if {$type eq "_call"} {
- set get_next 0
- }
- set t ""
- if {$type eq "_call"} {
- append body { [}
- append body [lindex $funcl $idx]
- append body { ]}
- } else {
- append body { [}
- if {$idx == 3} {
- set cmdlist_pre [list]
- } else {
- set cmdlist_pre [lrange $funcl 3 $idx-1]
- }
- append body $cmdlist_pre
- set t [lrange $funcl $idx+1 end]
- lappend tails $t
- lappend tails { ]}
- }
- incr i
- }
- append body [join [lreverse $tails] " "]
- #puts stdout "tails: $tails"
-
- return $body
- }
-
-
- interp alias "" o_of "" funcl::o_of_n 1
-
- #o_of_n
- #tcl list rep o combinator
- #
- # can take lists of ordinary commandlists, scripts and funcls
- # _fn 1 x where 1 indicates number of subfuncls and where x indicates next funcl position (_fn list or _arg)
- # _fn 0 indicates next item is an unwrapped commandlist (terminal command)
- #
- #o_of is equivalent to o_of_n 1 (1 argument o combinator)
- #last n args are passed to the prior function
- #e.g for n=1 f a b = f(a(b))
- #e.g for n=2, e f a b = e(f(a b))
- proc o_of_n {n args} {
- puts stdout "o_of_n '$args'"
- if {$n != 1} {
- error "o_of_n only implemented for 1 sub-funcl"
- }
- set comp [list] ;#composition list
- set end [lindex $args end]
- if {[lindex $end 0] in {_fn _call}]} {
- #is_funcl
- set endfunc [lindex $args end]
- } else {
- if {[llength $end] == 1 && [arg_is_script_shaped $end]} {
- #set endfunc [string map [list $end] {uplevel 1 [list if 1 ]}]
- set endfunc [list _call 1 3 [list uplevel 1 [list if 1 [lindex $end 0]]]]
- } else {
- set endfunc [list _call 1 3 [list {*}$end]]
- }
- }
-
- if {[llength $args] == 1} {
- return $endfunc
- }
- set comp $endfunc
- set revlist [lreverse [lrange $args 0 end-1]]
- foreach cmdlist $revlist {
- puts stderr "o_of_n >>-- $cmdlist"
- if {([llength $cmdlist] == 1) && [arg_is_script_shaped [lindex $cmdlist 0]]} {
- set is_script 1
- set script [lindex $cmdlist 0]
- set arglist [list data]
-
- set comp [list _fn 1 6 call_script $script $arglist $comp]
- } else {
- set posn1 [expr {[llength $cmdlist] + 2 + $n}]
- set comp [list _fn $n $posn1 {*}$cmdlist $comp]
- }
- }
- return $comp
- }
- proc call_script {script argnames args} {
- uplevel 3 [list if 1 [list apply [list $argnames $script] {*}$args]]
- }
- proc funcl_script_test {scr} {
- do_funcl_script_test $scr
- }
- proc do_funcl_script_test {scr} {
- #set j "in do_funcl_script_test"
- #set data "xxx"
- #puts '$scr'
- if 1 $scr
- }
-
- #standard o_ with no script-handling
- proc o_plain args {
- set body ""
- set i 0
- set tails [lrepeat [llength $args] ""]
- #puts stdout "tails: $tails"
- foreach cmdlist $args {
- set t ""
- if {$i > 0} {
- append body { [}
- }
- set posn [lsearch $cmdlist _]
- if {$posn <= 0} {
- append body $cmdlist
- if {$i == ([llength $args] -1)} {
- append body " \$data"
- }
- if {$i > 0} {
- set t {]}
- }
- } else {
- append body [lrange $cmdlist 0 $posn-1]
- if {$i == ([llength $args] -1)} {
- append body " \$data"
- }
- set t [lrange $cmdlist $posn+1 end]
- if {$i > 0} {
- append t { ]}
- }
- }
- lset tails $i $t
- incr i
- }
- append body [join [lreverse $tails] " "]
- #puts stdout "tails: $tails"
-
- return $body
- }
- #timings suggest no faster to split out the first item from the cmdlist loop
-}
-
-
-
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/http-2.10b1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/http-2.10b1.tm
deleted file mode 100644
index 6c3c068c..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/http-2.10b1.tm
+++ /dev/null
@@ -1,5457 +0,0 @@
-# http.tcl --
-#
-# Client-side HTTP for GET, POST, and HEAD commands. These routines can
-# be used in untrusted code that uses the Safesock security policy.
-# These procedures use a callback interface to avoid using vwait, which
-# is not defined in the safe base.
-#
-# See the file "license.terms" for information on usage and redistribution of
-# this file, and for a DISCLAIMER OF ALL WARRANTIES.
-
-package require Tcl 8.6-
-# Keep this in sync with pkgIndex.tcl and with the install directories in
-# Makefiles
-package provide http 2.10b1
-
-namespace eval http {
- # Allow resourcing to not clobber existing data
-
- variable http
- if {![info exists http]} {
- array set http {
- -accept */*
- -cookiejar {}
- -pipeline 1
- -postfresh 0
- -proxyhost {}
- -proxyport {}
- -proxyfilter http::ProxyRequired
- -proxynot {}
- -proxyauth {}
- -repost 0
- -threadlevel 0
- -urlencoding utf-8
- -zip 1
- }
- # We need a useragent string of this style or various servers will
- # refuse to send us compressed content even when we ask for it. This
- # follows the de-facto layout of user-agent strings in current browsers.
- # Safe interpreters do not have ::tcl_platform(os) or
- # ::tcl_platform(osVersion).
- if {[interp issafe]} {
- set http(-useragent) "Mozilla/5.0\
- (Windows; U;\
- Windows NT 10.0)\
- http/[package provide http] Tcl/[package provide Tcl]"
- } else {
- set http(-useragent) "Mozilla/5.0\
- ([string totitle $::tcl_platform(platform)]; U;\
- $::tcl_platform(os) $::tcl_platform(osVersion))\
- http/[package provide http] Tcl/[package provide Tcl]"
- }
- }
-
- proc init {} {
- # Set up the map for quoting chars. RFC3986 Section 2.3 say percent
- # encode all except: "... percent-encoded octets in the ranges of
- # ALPHA (%41-%5A and %61-%7A), DIGIT (%30-%39), hyphen (%2D), period
- # (%2E), underscore (%5F), or tilde (%7E) should not be created by URI
- # producers ..."
- for {set i 0} {$i <= 256} {incr i} {
- set c [format %c $i]
- if {![string match {[-._~a-zA-Z0-9]} $c]} {
- set map($c) %[format %.2X $i]
- }
- }
- # These are handled specially
- set map(\n) %0D%0A
- variable formMap [array get map]
-
- # Create a map for HTTP/1.1 open sockets
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
- if {[info exists socketMapping]} {
- # Close open sockets on re-init. Do not permit retries.
- foreach {url sock} [array get socketMapping] {
- unset -nocomplain socketClosing($url)
- unset -nocomplain socketPlayCmd($url)
- CloseSocket $sock
- }
- }
-
- # CloseSocket should have unset the socket* arrays, one element at
- # a time. Now unset anything that was overlooked.
- # Traces on "unset socketRdState(*)" will call CancelReadPipeline and
- # cancel any queued responses.
- # Traces on "unset socketWrState(*)" will call CancelWritePipeline and
- # cancel any queued requests.
- array unset socketMapping
- array unset socketRdState
- array unset socketWrState
- array unset socketRdQueue
- array unset socketWrQueue
- array unset socketPhQueue
- array unset socketClosing
- array unset socketPlayCmd
- array unset socketCoEvent
- array unset socketProxyId
- array set socketMapping {}
- array set socketRdState {}
- array set socketWrState {}
- array set socketRdQueue {}
- array set socketWrQueue {}
- array set socketPhQueue {}
- array set socketClosing {}
- array set socketPlayCmd {}
- array set socketCoEvent {}
- array set socketProxyId {}
- return
- }
- init
-
- variable urlTypes
- if {![info exists urlTypes]} {
- set urlTypes(http) [list 80 ::http::socket]
- }
-
- variable encodings [string tolower [encoding names]]
- # This can be changed, but iso8859-1 is the RFC standard.
- variable defaultCharset
- if {![info exists defaultCharset]} {
- set defaultCharset "iso8859-1"
- }
-
- # Force RFC 3986 strictness in geturl url verification?
- variable strict
- if {![info exists strict]} {
- set strict 1
- }
-
- # Let user control default keepalive for compatibility
- variable defaultKeepalive
- if {![info exists defaultKeepalive]} {
- set defaultKeepalive 0
- }
-
- # Regular expression used to parse cookies
- variable CookieRE {(?x) # EXPANDED SYNTAX
- \s* # Ignore leading spaces
- ([^][\u0000- ()<>@,;:\\""/?={}\u007f-\uffff]+) # Match the name
- = # LITERAL: Equal sign
- ([!\u0023-+\u002D-:<-\u005B\u005D-~]*) # Match the value
- (?:
- \s* ; \s* # LITERAL: semicolon
- ([^\u0000]+) # Match the options
- )?
- }
-
- variable TmpSockCounter 0
- variable ThreadCounter 0
-
- variable reasonDict [dict create {*}{
- 100 Continue
- 101 {Switching Protocols}
- 102 Processing
- 103 {Early Hints}
- 200 OK
- 201 Created
- 202 Accepted
- 203 {Non-Authoritative Information}
- 204 {No Content}
- 205 {Reset Content}
- 206 {Partial Content}
- 207 Multi-Status
- 208 {Already Reported}
- 226 {IM Used}
- 300 {Multiple Choices}
- 301 {Moved Permanently}
- 302 Found
- 303 {See Other}
- 304 {Not Modified}
- 305 {Use Proxy}
- 306 (Unused)
- 307 {Temporary Redirect}
- 308 {Permanent Redirect}
- 400 {Bad Request}
- 401 Unauthorized
- 402 {Payment Required}
- 403 Forbidden
- 404 {Not Found}
- 405 {Method Not Allowed}
- 406 {Not Acceptable}
- 407 {Proxy Authentication Required}
- 408 {Request Timeout}
- 409 Conflict
- 410 Gone
- 411 {Length Required}
- 412 {Precondition Failed}
- 413 {Content Too Large}
- 414 {URI Too Long}
- 415 {Unsupported Media Type}
- 416 {Range Not Satisfiable}
- 417 {Expectation Failed}
- 418 (Unused)
- 421 {Misdirected Request}
- 422 {Unprocessable Content}
- 423 Locked
- 424 {Failed Dependency}
- 425 {Too Early}
- 426 {Upgrade Required}
- 428 {Precondition Required}
- 429 {Too Many Requests}
- 431 {Request Header Fields Too Large}
- 451 {Unavailable For Legal Reasons}
- 500 {Internal Server Error}
- 501 {Not Implemented}
- 502 {Bad Gateway}
- 503 {Service Unavailable}
- 504 {Gateway Timeout}
- 505 {HTTP Version Not Supported}
- 506 {Variant Also Negotiates}
- 507 {Insufficient Storage}
- 508 {Loop Detected}
- 510 {Not Extended (OBSOLETED)}
- 511 {Network Authentication Required}
- }]
-
- variable failedProxyValues {
- binary
- body
- charset
- coding
- connection
- connectionRespFlag
- currentsize
- host
- http
- httpResponse
- meta
- method
- querylength
- queryoffset
- reasonPhrase
- requestHeaders
- requestLine
- responseCode
- state
- status
- tid
- totalsize
- transfer
- type
- }
-
- namespace export geturl config reset wait formatQuery postError quoteString
- namespace export register unregister registerError
- namespace export requestLine requestHeaders requestHeaderValue
- namespace export responseLine responseHeaders responseHeaderValue
- namespace export responseCode responseBody responseInfo reasonPhrase
- # - Legacy aliases, were never exported:
- # data, code, mapReply, meta, ncode
- # - Callable from outside (e.g. from TLS) by fully-qualified name, but
- # not exported:
- # socket
- # - Useful, but never exported (and likely to have naming collisions):
- # size, status, cleanup, error, init
- # Comments suggest that "init" can be used for re-initialisation,
- # although the command is undocumented.
- # - Never exported, renamed from lower-case names:
- # GetTextLine, MakeTransformationChunked.
-}
-
-# http::Log --
-#
-# Debugging output -- define this to observe HTTP/1.1 socket usage.
-# Should echo any args received.
-#
-# Arguments:
-# msg Message to output
-#
-if {[info command http::Log] eq {}} {proc http::Log {args} {}}
-
-# http::register --
-#
-# See documentation for details.
-#
-# Arguments:
-# proto URL protocol prefix, e.g. https
-# port Default port for protocol
-# command Command to use to create socket
-# Results:
-# list of port and command that was registered.
-
-proc http::register {proto port command} {
- variable urlTypes
- set urlTypes([string tolower $proto]) [list $port $command]
-}
-
-# http::unregister --
-#
-# Unregisters URL protocol handler
-#
-# Arguments:
-# proto URL protocol prefix, e.g. https
-# Results:
-# list of port and command that was unregistered.
-
-proc http::unregister {proto} {
- variable urlTypes
- set lower [string tolower $proto]
- if {![info exists urlTypes($lower)]} {
- return -code error "unsupported url type \"$proto\""
- }
- set old $urlTypes($lower)
- unset urlTypes($lower)
- return $old
-}
-
-# http::config --
-#
-# See documentation for details.
-#
-# Arguments:
-# args Options parsed by the procedure.
-# Results:
-# TODO
-
-proc http::config {args} {
- variable http
- set options [lsort [array names http -*]]
- set usage [join $options ", "]
- if {[llength $args] == 0} {
- set result {}
- foreach name $options {
- lappend result $name $http($name)
- }
- return $result
- }
- set options [string map {- ""} $options]
- set pat ^-(?:[join $options |])$
- if {[llength $args] == 1} {
- set flag [lindex $args 0]
- if {![regexp -- $pat $flag]} {
- return -code error "Unknown option $flag, must be: $usage"
- }
- return $http($flag)
- } elseif {[llength $args] % 2} {
- return -code error "If more than one argument is supplied, the\
- number of arguments must be even"
- } else {
- foreach {flag value} $args {
- if {![regexp -- $pat $flag]} {
- return -code error "Unknown option $flag, must be: $usage"
- }
- if {($flag eq {-threadlevel}) && ($value ni {0 1 2})} {
- return -code error {Option -threadlevel must be 0, 1 or 2}
- }
- set http($flag) $value
- }
- return
- }
-}
-
-# ------------------------------------------------------------------------------
-# Proc http::reasonPhrase
-# ------------------------------------------------------------------------------
-# Command to return the IANA-recommended "reason phrase" for a HTTP Status Code.
-# Information obtained from:
-# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
-#
-# Arguments:
-# code - A valid HTTP Status Code (integer from 100 to 599)
-#
-# Return Value: the reason phrase
-# ------------------------------------------------------------------------------
-
-proc http::reasonPhrase {code} {
- variable reasonDict
- if {![regexp -- {^[1-5][0-9][0-9]$} $code]} {
- set msg {argument must be a three-digit integer from 100 to 599}
- return -code error $msg
- }
- if {[dict exists $reasonDict $code]} {
- set reason [dict get $reasonDict $code]
- } else {
- set reason Unassigned
- }
- return $reason
-}
-
-# http::Finish --
-#
-# Clean up the socket and eval close time callbacks
-#
-# Arguments:
-# token Connection token.
-# errormsg (optional) If set, forces status to error.
-# skipCB (optional) If set, don't call the -command callback. This
-# is useful when geturl wants to throw an exception instead
-# of calling the callback. That way, the same error isn't
-# reported to two places.
-#
-# Side Effects:
-# May close the socket.
-
-proc http::Finish {token {errormsg ""} {skipCB 0}} {
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- variable $token
- upvar 0 $token state
- global errorInfo errorCode
- set closeQueue 0
- if {$errormsg ne ""} {
- set state(error) [list $errormsg $errorInfo $errorCode]
- set state(status) "error"
- }
- if {[info commands ${token}--EventCoroutine] ne {}} {
- rename ${token}--EventCoroutine {}
- }
- if {[info commands ${token}--SocketCoroutine] ne {}} {
- rename ${token}--SocketCoroutine {}
- }
- if {[info exists state(socketcoro)]} {
- Log $token Cancel socket after-idle event (Finish)
- after cancel $state(socketcoro)
- unset state(socketcoro)
- }
-
- # Is this an upgrade request/response?
- set upgradeResponse \
- [expr { [info exists state(upgradeRequest)]
- && $state(upgradeRequest)
- && [info exists state(http)]
- && ([ncode $token] eq {101})
- && [info exists state(connection)]
- && ("upgrade" in $state(connection))
- && [info exists state(upgrade)]
- && ("" ne $state(upgrade))
- }]
-
- if { ($state(status) eq "timeout")
- || ($state(status) eq "error")
- || ($state(status) eq "eof")
- } {
- set closeQueue 1
- set connId $state(socketinfo)
- if {[info exists state(sock)]} {
- set sock $state(sock)
- CloseSocket $state(sock) $token
- } else {
- # When opening the socket and calling http::reset
- # immediately, the socket may not yet exist.
- # Test http-4.11 may come here.
- }
- if {$state(tid) ne {}} {
- # When opening the socket in a thread, and calling http::reset
- # immediately, the thread may still exist.
- # Test http-4.11 may come here.
- thread::release $state(tid)
- set state(tid) {}
- } else {
- }
- } elseif {$upgradeResponse} {
- # Special handling for an upgrade request/response.
- # - geturl ensures that this is not a "persistent" socket used for
- # multiple HTTP requests, so a call to KeepSocket is not needed.
- # - Leave socket open, so a call to CloseSocket is not needed either.
- # - Remove fileevent bindings. The caller will set its own bindings.
- # - THE CALLER MUST PROCESS THE UPGRADED SOCKET IN THE CALLBACK COMMAND
- # PASSED TO http::geturl AS -command callback.
- catch {fileevent $state(sock) readable {}}
- catch {fileevent $state(sock) writable {}}
- } elseif {
- ([info exists state(-keepalive)] && !$state(-keepalive))
- || ([info exists state(connection)] && ("close" in $state(connection)))
- } {
- set closeQueue 1
- set connId $state(socketinfo)
- if {[info exists state(sock)]} {
- set sock $state(sock)
- CloseSocket $state(sock) $token
- } else {
- # When opening the socket and calling http::reset
- # immediately, the socket may not yet exist.
- # Test http-4.11 may come here.
- }
- } elseif {
- ([info exists state(-keepalive)] && $state(-keepalive))
- && ([info exists state(connection)] && ("close" ni $state(connection)))
- } {
- KeepSocket $token
- }
- if {[info exists state(after)]} {
- after cancel $state(after)
- unset state(after)
- }
- if {[info exists state(-command)] && (!$skipCB)
- && (![info exists state(done-command-cb)])} {
- set state(done-command-cb) yes
- if { [catch {namespace eval :: $state(-command) $token} err]
- && ($errormsg eq "")
- } {
- set state(error) [list $err $errorInfo $errorCode]
- set state(status) error
- }
- }
-
- if { $closeQueue
- && [info exists socketMapping($connId)]
- && ($socketMapping($connId) eq $sock)
- } {
- http::CloseQueuedQueries $connId $token
- # This calls Unset. Other cases do not need the call.
- }
- return
-}
-
-# http::KeepSocket -
-#
-# Keep a socket in the persistent sockets table and connect it to its next
-# queued task if possible. Otherwise leave it idle and ready for its next
-# use.
-#
-# If $socketClosing(*), then ("close" in $state(connection)) and therefore
-# this command will not be called by Finish.
-#
-# Arguments:
-# token Connection token.
-
-proc http::KeepSocket {token} {
- variable http
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
-
- # Keep this socket open for another request ("Keep-Alive").
- # React if the server half-closes the socket.
- # Discussion is in http::geturl.
- catch {fileevent $state(sock) readable [list http::CheckEof $state(sock)]}
-
- # The line below should not be changed in production code.
- # It is edited by the test suite.
- set TEST_EOF 0
- if {$TEST_EOF} {
- # ONLY for testing reaction to server eof.
- # No server timeouts will be caught.
- catch {fileevent $state(sock) readable {}}
- }
-
- if { [info exists state(socketinfo)]
- && [info exists socketMapping($state(socketinfo))]
- } {
- set connId $state(socketinfo)
- # The value "Rready" is set only here.
- set socketRdState($connId) Rready
-
- if { $state(-pipeline)
- && [info exists socketRdQueue($connId)]
- && [llength $socketRdQueue($connId)]
- } {
- # The usual case for pipelined responses - if another response is
- # queued, arrange to read it.
- set token3 [lindex $socketRdQueue($connId) 0]
- set socketRdQueue($connId) [lrange $socketRdQueue($connId) 1 end]
-
- #Log pipelined, GRANT read access to $token3 in KeepSocket
- set socketRdState($connId) $token3
- ReceiveResponse $token3
-
- # Other pipelined cases.
- # - The test above ensures that, for the pipelined cases in the two
- # tests below, the read queue is empty.
- # - In those two tests, check whether the next write will be
- # nonpipeline.
- } elseif {
- $state(-pipeline)
- && [info exists socketWrState($connId)]
- && ($socketWrState($connId) eq "peNding")
-
- && [info exists socketWrQueue($connId)]
- && [llength $socketWrQueue($connId)]
- && (![set token3 [lindex $socketWrQueue($connId) 0]
- set ${token3}(-pipeline)
- ]
- )
- } {
- # This case:
- # - Now it the time to run the "pending" request.
- # - The next token in the write queue is nonpipeline, and
- # socketWrState has been marked "pending" (in
- # http::NextPipelinedWrite or http::geturl) so a new pipelined
- # request cannot jump the queue.
- #
- # Tests:
- # - In this case the read queue (tested above) is empty and this
- # "pending" write token is in front of the rest of the write
- # queue.
- # - The write state is not Wready and therefore appears to be busy,
- # but because it is "pending" we know that it is reserved for the
- # first item in the write queue, a non-pipelined request that is
- # waiting for the read queue to empty. That has now happened: so
- # give that request read and write access.
- set conn [set ${token3}(connArgs)]
- #Log nonpipeline, GRANT r/w access to $token3 in KeepSocket
- set socketRdState($connId) $token3
- set socketWrState($connId) $token3
- set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end]
- # Connect does its own fconfigure.
- fileevent $state(sock) writable [list http::Connect $token3 {*}$conn]
- #Log ---- $state(sock) << conn to $token3 for HTTP request (c)
-
- } elseif {
- $state(-pipeline)
- && [info exists socketWrState($connId)]
- && ($socketWrState($connId) eq "peNding")
-
- } {
- # Should not come here. The second block in the previous "elseif"
- # test should be tautologous (but was needed in an earlier
- # implementation) and will be removed after testing.
- # If we get here, the value "pending" was assigned in error.
- # This error would block the queue for ever.
- Log ^X$tk <<<<< Error in queueing of requests >>>>> - token $token
-
- } elseif {
- $state(-pipeline)
- && [info exists socketWrState($connId)]
- && ($socketWrState($connId) eq "Wready")
-
- && [info exists socketWrQueue($connId)]
- && [llength $socketWrQueue($connId)]
- && (![set token3 [lindex $socketWrQueue($connId) 0]
- set ${token3}(-pipeline)
- ]
- )
- } {
- # This case:
- # - The next token in the write queue is nonpipeline, and
- # socketWrState is Wready. Get the next event from socketWrQueue.
- # Tests:
- # - In this case the read state (tested above) is Rready and the
- # write state (tested here) is Wready - there is no "pending"
- # request.
- # Code:
- # - The code is the same as the code below for the nonpipelined
- # case with a queued request.
- set conn [set ${token3}(connArgs)]
- #Log nonpipeline, GRANT r/w access to $token3 in KeepSocket
- set socketRdState($connId) $token3
- set socketWrState($connId) $token3
- set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end]
- # Connect does its own fconfigure.
- fileevent $state(sock) writable [list http::Connect $token3 {*}$conn]
- #Log ---- $state(sock) << conn to $token3 for HTTP request (c)
-
- } elseif {
- (!$state(-pipeline))
- && [info exists socketWrQueue($connId)]
- && [llength $socketWrQueue($connId)]
- && ("close" ni $state(connection))
- } {
- # If not pipelined, (socketRdState eq Rready) tells us that we are
- # ready for the next write - there is no need to check
- # socketWrState. Write the next request, if one is waiting.
- # If the next request is pipelined, it receives premature read
- # access to the socket. This is not a problem.
- set token3 [lindex $socketWrQueue($connId) 0]
- set conn [set ${token3}(connArgs)]
- #Log nonpipeline, GRANT r/w access to $token3 in KeepSocket
- set socketRdState($connId) $token3
- set socketWrState($connId) $token3
- set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end]
- # Connect does its own fconfigure.
- fileevent $state(sock) writable [list http::Connect $token3 {*}$conn]
- #Log ---- $state(sock) << conn to $token3 for HTTP request (d)
-
- } elseif {(!$state(-pipeline))} {
- set socketWrState($connId) Wready
- # Rready and Wready and idle: nothing to do.
- }
-
- } else {
- CloseSocket $state(sock) $token
- # There is no socketMapping($state(socketinfo)), so it does not matter
- # that CloseQueuedQueries is not called.
- }
- return
-}
-
-# http::CheckEof -
-#
-# Read from a socket and close it if eof.
-# The command is bound to "fileevent readable" on an idle socket, and
-# "eof" is the only event that should trigger the binding, occurring when
-# the server times out and half-closes the socket.
-#
-# A read is necessary so that [eof] gives a meaningful result.
-# Any bytes sent are junk (or a bug).
-
-proc http::CheckEof {sock} {
- set junk [read $sock]
- set n [string length $junk]
- if {$n} {
- Log "WARNING: $n bytes received but no HTTP request sent"
- }
-
- if {[catch {eof $sock} res] || $res} {
- # The server has half-closed the socket.
- # If a new write has started, its transaction will fail and
- # will then be error-handled.
- CloseSocket $sock
- }
- return
-}
-
-# http::CloseSocket -
-#
-# Close a socket and remove it from the persistent sockets table. If
-# possible an http token is included here but when we are called from a
-# fileevent on remote closure we need to find the correct entry - hence
-# the "else" block of the first "if" command.
-
-proc http::CloseSocket {s {token {}}} {
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- set tk [namespace tail $token]
-
- catch {fileevent $s readable {}}
- set connId {}
- if {$token ne ""} {
- variable $token
- upvar 0 $token state
- if {[info exists state(socketinfo)]} {
- set connId $state(socketinfo)
- }
- } else {
- set map [array get socketMapping]
- set ndx [lsearch -exact $map $s]
- if {$ndx >= 0} {
- incr ndx -1
- set connId [lindex $map $ndx]
- }
- }
- if { ($connId ne {})
- && [info exists socketMapping($connId)]
- && ($socketMapping($connId) eq $s)
- } {
- Log "Closing connection $connId (sock $socketMapping($connId))"
- if {[catch {close $socketMapping($connId)} err]} {
- Log "Error closing connection: $err"
- } else {
- }
- if {$token eq {}} {
- # Cases with a non-empty token are handled by Finish, so the tokens
- # are finished in connection order.
- http::CloseQueuedQueries $connId
- } else {
- }
- } else {
- Log "Closing socket $s (no connection info)"
- if {[catch {close $s} err]} {
- Log "Error closing socket: $err"
- } else {
- }
- }
- return
-}
-
-# http::CloseQueuedQueries
-#
-# connId - identifier "domain:port" for the connection
-# token - (optional) used only for logging
-#
-# Called from http::CloseSocket and http::Finish, after a connection is closed,
-# to clear the read and write queues if this has not already been done.
-
-proc http::CloseQueuedQueries {connId {token {}}} {
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- ##Log CloseQueuedQueries $connId $token
- if {![info exists socketMapping($connId)]} {
- # Command has already been called.
- # Don't come here again - especially recursively.
- return
- }
-
- # Used only for logging.
- if {$token eq {}} {
- set tk {}
- } else {
- set tk [namespace tail $token]
- }
-
- if { [info exists socketPlayCmd($connId)]
- && ($socketPlayCmd($connId) ne {ReplayIfClose Wready {} {}})
- } {
- # Before unsetting, there is some unfinished business.
- # - If the server sent "Connection: close", we have stored the command
- # for retrying any queued requests in socketPlayCmd, so copy that
- # value for execution below. socketClosing(*) was also set.
- # - Also clear the queues to prevent calls to Finish that would set the
- # state for the requests that will be retried to "finished with error
- # status".
- # - At this stage socketPhQueue is empty.
- set unfinished $socketPlayCmd($connId)
- set socketRdQueue($connId) {}
- set socketWrQueue($connId) {}
- } else {
- set unfinished {}
- }
-
- Unset $connId
-
- if {$unfinished ne {}} {
- Log ^R$tk Any unfinished transactions (excluding $token) failed \
- - token $token - unfinished $unfinished
- {*}$unfinished
- # Calls ReplayIfClose.
- }
- return
-}
-
-# http::Unset
-#
-# The trace on "unset socketRdState(*)" will call CancelReadPipeline
-# and cancel any queued responses.
-# The trace on "unset socketWrState(*)" will call CancelWritePipeline
-# and cancel any queued requests.
-
-proc http::Unset {connId} {
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- unset socketMapping($connId)
- unset socketRdState($connId)
- unset socketWrState($connId)
- unset -nocomplain socketRdQueue($connId)
- unset -nocomplain socketWrQueue($connId)
- unset -nocomplain socketClosing($connId)
- unset -nocomplain socketPlayCmd($connId)
- unset -nocomplain socketProxyId($connId)
- return
-}
-
-# http::reset --
-#
-# See documentation for details.
-#
-# Arguments:
-# token Connection token.
-# why Status info.
-#
-# Side Effects:
-# See Finish
-
-proc http::reset {token {why reset}} {
- variable $token
- upvar 0 $token state
- set state(status) $why
- catch {fileevent $state(sock) readable {}}
- catch {fileevent $state(sock) writable {}}
- Finish $token
- if {[info exists state(error)]} {
- set errorlist $state(error)
- unset state
- eval ::error $errorlist
- # i.e. error msg errorInfo errorCode
- }
- return
-}
-
-# http::geturl --
-#
-# Establishes a connection to a remote url via http.
-#
-# Arguments:
-# url The http URL to goget.
-# args Option value pairs. Valid options include:
-# -blocksize, -validate, -headers, -timeout
-# Results:
-# Returns a token for this connection. This token is the name of an
-# array that the caller should unset to garbage collect the state.
-
-proc http::geturl {url args} {
- variable urlTypes
-
- # - If ::tls::socketCmd has its default value "::socket", change it to the
- # new value ::http::socketForTls.
- # - If the old value is different, then it has been modified either by the
- # script or by the Tcl installation, and replaced by a new command. The
- # script or installation that modified ::tls::socketCmd is also
- # responsible for integrating ::http::socketForTls into its own "new"
- # command, if it wishes to do so.
- # - Commands that open a socket:
- # - ::socket - basic
- # - ::http::socket - can use a thread to avoid blockage by slow DNS
- # lookup. See http::config option -threadlevel.
- # - ::http::socketForTls - as ::http::socket, but can also open a socket
- # for HTTPS/TLS through a proxy.
-
- if {[info exists ::tls::socketCmd] && ($::tls::socketCmd eq {::socket})} {
- set ::tls::socketCmd ::http::socketForTls
- }
-
- set token [CreateToken $url {*}$args]
- variable $token
- upvar 0 $token state
-
- AsyncTransaction $token
-
- # --------------------------------------------------------------------------
- # Synchronous Call to http::geturl
- # --------------------------------------------------------------------------
- # - If the call to http::geturl is asynchronous, it is now complete (apart
- # from delivering the return value).
- # - If the call to http::geturl is synchronous, the command must now wait
- # for the HTTP transaction to be completed. The call to http::wait uses
- # vwait, which may be inappropriate if the caller makes other HTTP
- # requests in the background.
- # --------------------------------------------------------------------------
-
- if {![info exists state(-command)]} {
- # geturl does EVERYTHING asynchronously, so if the user
- # calls it synchronously, we just do a wait here.
- http::wait $token
-
- if {![info exists state]} {
- # If we timed out then Finish has been called and the users
- # command callback may have cleaned up the token. If so we end up
- # here with nothing left to do.
- return $token
- } elseif {$state(status) eq "error"} {
- # Something went wrong while trying to establish the connection.
- # Clean up after events and such, but DON'T call the command
- # callback (if available) because we're going to throw an
- # exception from here instead.
- set err [lindex $state(error) 0]
- cleanup $token
- return -code error $err
- }
- }
-
- return $token
-}
-
-# ------------------------------------------------------------------------------
-# Proc http::CreateToken
-# ------------------------------------------------------------------------------
-# Command to convert arguments into an initialised request token.
-# The return value is the variable name of the token.
-#
-# Other effects:
-# - Sets ::http::http(usingThread) if not already done
-# - Sets ::http::http(uid) if not already done
-# - Increments ::http::http(uid)
-# - May increment ::http::TmpSockCounter
-# - Alters ::http::socketPlayCmd, ::http::socketWrQueue if a -keepalive 1
-# request is appended to the queue of a persistent socket that is already
-# scheduled to close.
-# This also sets state(alreadyQueued) to 1.
-# - Alters ::http::socketPhQueue if a -keepalive 1 request is appended to the
-# queue of a persistent socket that has not yet been created (and is therefore
-# represented by a placeholder).
-# This also sets state(ReusingPlaceholder) to 1.
-# ------------------------------------------------------------------------------
-
-proc http::CreateToken {url args} {
- variable http
- variable urlTypes
- variable defaultCharset
- variable defaultKeepalive
- variable strict
- variable TmpSockCounter
-
- # Initialize the state variable, an array. We'll return the name of this
- # array as the token for the transaction.
-
- if {![info exists http(usingThread)]} {
- set http(usingThread) 0
- }
- if {![info exists http(uid)]} {
- set http(uid) 0
- }
- set token [namespace current]::[incr http(uid)]
- ##Log Starting http::geturl - token $token
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
- reset $token
- Log ^A$tk URL $url - token $token
-
- # Process command options.
-
- array set state {
- -binary false
- -blocksize 8192
- -queryblocksize 8192
- -validate 0
- -headers {}
- -timeout 0
- -type application/x-www-form-urlencoded
- -queryprogress {}
- -protocol 1.1
- -guesstype 0
- binary 0
- state created
- meta {}
- method {}
- coding {}
- currentsize 0
- totalsize 0
- querylength 0
- queryoffset 0
- type application/octet-stream
- body {}
- status ""
- http ""
- httpResponse {}
- responseCode {}
- reasonPhrase {}
- connection keep-alive
- tid {}
- requestHeaders {}
- requestLine {}
- transfer {}
- proxyUsed none
- }
- set state(-keepalive) $defaultKeepalive
- set state(-strict) $strict
- # These flags have their types verified [Bug 811170]
- array set type {
- -binary boolean
- -blocksize integer
- -guesstype boolean
- -queryblocksize integer
- -strict boolean
- -timeout integer
- -validate boolean
- -headers list
- }
- set state(charset) $defaultCharset
- set options {
- -binary -blocksize -channel -command -guesstype -handler -headers -keepalive
- -method -myaddr -progress -protocol -query -queryblocksize
- -querychannel -queryprogress -strict -timeout -type -validate
- }
- set usage [join [lsort $options] ", "]
- set options [string map {- ""} $options]
- set pat ^-(?:[join $options |])$
- foreach {flag value} $args {
- if {[regexp -- $pat $flag]} {
- # Validate numbers
- if { [info exists type($flag)]
- && (![string is $type($flag) -strict $value])
- } {
- unset $token
- return -code error \
- "Bad value for $flag ($value), must be $type($flag)"
- }
- if {($flag eq "-headers") && ([llength $value] % 2 != 0)} {
- unset $token
- return -code error "Bad value for $flag ($value), number\
- of list elements must be even"
- }
- set state($flag) $value
- } else {
- unset $token
- return -code error "Unknown option $flag, can be: $usage"
- }
- }
-
- # Make sure -query and -querychannel aren't both specified
-
- set isQueryChannel [info exists state(-querychannel)]
- set isQuery [info exists state(-query)]
- if {$isQuery && $isQueryChannel} {
- unset $token
- return -code error "Can't combine -query and -querychannel options!"
- }
-
- # Validate URL, determine the server host and port, and check proxy case
- # Recognize user:pass@host URLs also, although we do not do anything with
- # that info yet.
-
- # URLs have basically four parts.
- # First, before the colon, is the protocol scheme (e.g. http)
- # Second, for HTTP-like protocols, is the authority
- # The authority is preceded by // and lasts up to (but not including)
- # the following / or ? and it identifies up to four parts, of which
- # only one, the host, is required (if an authority is present at all).
- # All other parts of the authority (user name, password, port number)
- # are optional.
- # Third is the resource name, which is split into two parts at a ?
- # The first part (from the single "/" up to "?") is the path, and the
- # second part (from that "?" up to "#") is the query. *HOWEVER*, we do
- # not need to separate them; we send the whole lot to the server.
- # Both, path and query are allowed to be missing, including their
- # delimiting character.
- # Fourth is the fragment identifier, which is everything after the first
- # "#" in the URL. The fragment identifier MUST NOT be sent to the server
- # and indeed, we don't bother to validate it (it could be an error to
- # pass it in here, but it's cheap to strip).
- #
- # An example of a URL that has all the parts:
- #
- # http://jschmoe:xyzzy@www.bogus.net:8000/foo/bar.tml?q=foo#changes
- #
- # The "http" is the protocol, the user is "jschmoe", the password is
- # "xyzzy", the host is "www.bogus.net", the port is "8000", the path is
- # "/foo/bar.tml", the query is "q=foo", and the fragment is "changes".
- #
- # Note that the RE actually combines the user and password parts, as
- # recommended in RFC 3986. Indeed, that RFC states that putting passwords
- # in URLs is a Really Bad Idea, something with which I would agree utterly.
- # RFC 9110 Sec 4.2.4 goes further than this, and deprecates the format
- # "user:password@". It is retained here for backward compatibility,
- # but its use is not recommended.
- #
- # From a validation perspective, we need to ensure that the parts of the
- # URL that are going to the server are correctly encoded. This is only
- # done if $state(-strict) is true (inherited from $::http::strict).
-
- set URLmatcher {(?x) # this is _expanded_ syntax
- ^
- (?: (\w+) : ) ? #
- (?: //
- (?:
- (
- [^@/\#?]+ #
- ) @
- )?
- ( #
- [^/:\#?]+ | # host name or IPv4 address
- \[ [^/\#?]+ \] # IPv6 address in square brackets
- )
- (?: : (\d+) )? #
- )?
- ( [/\?] [^\#]*)? # (including query)
- (?: \# (.*) )? #
- $
- }
-
- # Phase one: parse
- if {![regexp -- $URLmatcher $url -> proto user host port srvurl]} {
- unset $token
- return -code error "Unsupported URL: $url"
- }
- # Phase two: validate
- set host [string trim $host {[]}]; # strip square brackets from IPv6 address
- if {$host eq ""} {
- # Caller has to provide a host name; we do not have a "default host"
- # that would enable us to handle relative URLs.
- unset $token
- return -code error "Missing host part: $url"
- # Note that we don't check the hostname for validity here; if it's
- # invalid, we'll simply fail to resolve it later on.
- }
- if {$port ne "" && $port > 65535} {
- unset $token
- return -code error "Invalid port number: $port"
- }
- # The user identification and resource identification parts of the URL can
- # have encoded characters in them; take care!
- if {$user ne ""} {
- # Check for validity according to RFC 3986, Appendix A
- set validityRE {(?xi)
- ^
- (?: [-\w.~!$&'()*+,;=:] | %[0-9a-f][0-9a-f] )+
- $
- }
- if {$state(-strict) && ![regexp -- $validityRE $user]} {
- unset $token
- # Provide a better error message in this error case
- if {[regexp {(?i)%(?![0-9a-f][0-9a-f]).?.?} $user bad]} {
- return -code error \
- "Illegal encoding character usage \"$bad\" in URL user"
- }
- return -code error "Illegal characters in URL user"
- }
- }
- if {$srvurl ne ""} {
- # RFC 3986 allows empty paths (not even a /), but servers
- # return 400 if the path in the HTTP request doesn't start
- # with / , so add it here if needed.
- if {[string index $srvurl 0] ne "/"} {
- set srvurl /$srvurl
- }
- # Check for validity according to RFC 3986, Appendix A
- set validityRE {(?xi)
- ^
- # Path part (already must start with / character)
- (?: [-\w.~!$&'()*+,;=:@/] | %[0-9a-f][0-9a-f] )*
- # Query part (optional, permits ? characters)
- (?: \? (?: [-\w.~!$&'()*+,;=:@/?] | %[0-9a-f][0-9a-f] )* )?
- $
- }
- if {$state(-strict) && ![regexp -- $validityRE $srvurl]} {
- unset $token
- # Provide a better error message in this error case
- if {[regexp {(?i)%(?![0-9a-f][0-9a-f])..} $srvurl bad]} {
- return -code error \
- "Illegal encoding character usage \"$bad\" in URL path"
- }
- return -code error "Illegal characters in URL path"
- }
- if {![regexp {^[^?#]+} $srvurl state(path)]} {
- set state(path) /
- }
- } else {
- set srvurl /
- set state(path) /
- }
- if {$proto eq ""} {
- set proto http
- }
- set lower [string tolower $proto]
- if {![info exists urlTypes($lower)]} {
- unset $token
- return -code error "Unsupported URL type \"$proto\""
- }
- set defport [lindex $urlTypes($lower) 0]
- set defcmd [lindex $urlTypes($lower) 1]
-
- if {$port eq ""} {
- set port $defport
- }
- if {![catch {$http(-proxyfilter) $host} proxy]} {
- set phost [lindex $proxy 0]
- set pport [lindex $proxy 1]
- } else {
- set phost {}
- set pport {}
- }
-
- # OK, now reassemble into a full URL
- set url ${proto}://
- if {$user ne ""} {
- append url $user
- append url @
- }
- append url $host
- if {$port != $defport} {
- append url : $port
- }
- append url $srvurl
- # Don't append the fragment! RFC 7230 Sec 5.1
- set state(url) $url
-
- # Proxy connections aren't shared among different hosts.
- set state(socketinfo) $host:$port
-
- # Save the accept types at this point to prevent a race condition. [Bug
- # c11a51c482]
- set state(accept-types) $http(-accept)
-
- # Check whether this is an Upgrade request.
- set connectionValues [SplitCommaSeparatedFieldValue \
- [GetFieldValue $state(-headers) Connection]]
- set connectionValues [string tolower $connectionValues]
- set upgradeValues [SplitCommaSeparatedFieldValue \
- [GetFieldValue $state(-headers) Upgrade]]
- set state(upgradeRequest) [expr { "upgrade" in $connectionValues
- && [llength $upgradeValues] >= 1}]
- set state(connectionValues) $connectionValues
-
- if {$isQuery || $isQueryChannel} {
- # It's a POST.
- # A client wishing to send a non-idempotent request SHOULD wait to send
- # that request until it has received the response status for the
- # previous request.
- if {$http(-postfresh)} {
- # Override -keepalive for a POST. Use a new connection, and thus
- # avoid the small risk of a race against server timeout.
- set state(-keepalive) 0
- } else {
- # Allow -keepalive but do not -pipeline - wait for the previous
- # transaction to finish.
- # There is a small risk of a race against server timeout.
- set state(-pipeline) 0
- }
- } elseif {$state(upgradeRequest)} {
- # It's an upgrade request. Method must be GET (untested).
- # Force -keepalive to 0 so the connection is not made over a persistent
- # socket, i.e. one used for multiple HTTP requests.
- set state(-keepalive) 0
- } else {
- # It's a non-upgrade GET or HEAD.
- set state(-pipeline) $http(-pipeline)
- }
-
- # We cannot handle chunked encodings with -handler, so force HTTP/1.0
- # until we can manage this.
- if {[info exists state(-handler)]} {
- set state(-protocol) 1.0
- }
-
- # RFC 7320 A.1 - HTTP/1.0 Keep-Alive is problematic. We do not support it.
- if {$state(-protocol) eq "1.0"} {
- set state(connection) close
- set state(-keepalive) 0
- }
-
- # Handle proxy requests here for http:// but not for https://
- # The proxying for https is done in the ::http::socketForTls command.
- # A proxy request for http:// needs the full URL in the HTTP request line,
- # including the server name.
- # The *tls* test below attempts to describe protocols in addition to
- # "https on port 443" that use HTTP over TLS.
- if {($phost ne "") && (![string match -nocase *tls* $defcmd])} {
- set srvurl $url
- set targetAddr [list $phost $pport]
- set state(proxyUsed) HttpProxy
- # The value of state(proxyUsed) none|HttpProxy depends only on the
- # all-transactions http::config settings and on the target URL.
- # Even if this is a persistent socket there is no need to change the
- # value of state(proxyUsed) for other transactions that use the socket:
- # they have the same value already.
- } else {
- set targetAddr [list $host $port]
- }
-
- set sockopts [list -async]
-
- # Pass -myaddr directly to the socket command
- if {[info exists state(-myaddr)]} {
- lappend sockopts -myaddr $state(-myaddr)
- }
-
- set state(connArgs) [list $proto $phost $srvurl]
- set state(openCmd) [list {*}$defcmd {*}$sockopts -type $token {*}$targetAddr]
-
- # See if we are supposed to use a previously opened channel.
- # - In principle, ANY call to http::geturl could use a previously opened
- # channel if it is available - the "Connection: keep-alive" header is a
- # request to leave the channel open AFTER completion of this call.
- # - In fact, we try to use an existing channel only if -keepalive 1 -- this
- # means that at most one channel is left open for each value of
- # $state(socketinfo). This property simplifies the mapping of open
- # channels.
- set reusing 0
- set state(alreadyQueued) 0
- set state(ReusingPlaceholder) 0
- if {$state(-keepalive)} {
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- if {[info exists socketMapping($state(socketinfo))]} {
- # - If the connection is idle, it has a "fileevent readable" binding
- # to http::CheckEof, in case the server times out and half-closes
- # the socket (http::CheckEof closes the other half).
- # - We leave this binding in place until just before the last
- # puts+flush in http::Connected (GET/HEAD) or http::Write (POST),
- # after which the HTTP response might be generated.
-
- if { [info exists socketClosing($state(socketinfo))]
- && $socketClosing($state(socketinfo))
- } {
- # socketClosing(*) is set because the server has sent a
- # "Connection: close" header.
- # Do not use the persistent socket again.
- # Since we have only one persistent socket per server, and the
- # old socket is not yet dead, add the request to the write queue
- # of the dying socket, which will be replayed by ReplayIfClose.
- # Also add it to socketWrQueue(*) which is used only if an error
- # causes a call to Finish.
- set reusing 1
- set sock $socketMapping($state(socketinfo))
- set state(proxyUsed) $socketProxyId($state(socketinfo))
- Log "reusing closing socket $sock for $state(socketinfo) - token $token"
-
- set state(alreadyQueued) 1
- lassign $socketPlayCmd($state(socketinfo)) com0 com1 com2 com3
- lappend com3 $token
- set socketPlayCmd($state(socketinfo)) [list $com0 $com1 $com2 $com3]
- lappend socketWrQueue($state(socketinfo)) $token
- ##Log socketPlayCmd($state(socketinfo)) is $socketPlayCmd($state(socketinfo))
- ##Log socketWrQueue($state(socketinfo)) is $socketWrQueue($state(socketinfo))
- } elseif {
- [catch {fconfigure $socketMapping($state(socketinfo))}]
- && (![SockIsPlaceHolder $socketMapping($state(socketinfo))])
- } {
- ###Log "Socket $socketMapping($state(socketinfo)) for $state(socketinfo)"
- # FIXME Is it still possible for this code to be executed? If
- # so, this could be another place to call TestForReplay,
- # rather than discarding the queued transactions.
- Log "WARNING: socket for $state(socketinfo) was closed\
- - token $token"
- Log "WARNING - if testing, pay special attention to this\
- case (GH) which is seldom executed - token $token"
-
- # This will call CancelReadPipeline, CancelWritePipeline, and
- # cancel any queued requests, responses.
- Unset $state(socketinfo)
- } else {
- # Use the persistent socket.
- # - The socket may not be ready to write: an earlier request might
- # still be still writing (in the pipelined case) or
- # writing/reading (in the nonpipeline case). This possibility
- # is handled by socketWrQueue later in this command.
- # - The socket may not yet exist, and be defined with a placeholder.
- set reusing 1
- set sock $socketMapping($state(socketinfo))
- set state(proxyUsed) $socketProxyId($state(socketinfo))
- if {[SockIsPlaceHolder $sock]} {
- set state(ReusingPlaceholder) 1
- lappend socketPhQueue($sock) $token
- } else {
- }
- Log "reusing open socket $sock for $state(socketinfo) - token $token"
- }
- # Do not automatically close the connection socket.
- set state(connection) keep-alive
- }
- }
-
- set state(reusing) $reusing
- unset reusing
-
- if {![info exists sock]} {
- # N.B. At this point ([info exists sock] == $state(reusing)).
- # This will no longer be true after we set a value of sock here.
- # Give the socket a placeholder name.
- set sock HTTP_PLACEHOLDER_[incr TmpSockCounter]
- }
- set state(sock) $sock
-
- if {$state(reusing)} {
- # Define these for use (only) by http::ReplayIfDead if the persistent
- # connection has died.
- set state(tmpConnArgs) $state(connArgs)
- set state(tmpState) [array get state]
- set state(tmpOpenCmd) $state(openCmd)
- }
- return $token
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc ::http::SockIsPlaceHolder
-# ------------------------------------------------------------------------------
-# Command to return 0 if the argument is a genuine socket handle, or 1 if is a
-# placeholder value generated by geturl or ReplayCore before the real socket is
-# created.
-#
-# Arguments:
-# sock - either a valid socket handle or a placeholder value
-#
-# Return Value: 0 or 1
-# ------------------------------------------------------------------------------
-
-proc http::SockIsPlaceHolder {sock} {
- expr {[string range $sock 0 16] eq {HTTP_PLACEHOLDER_}}
-}
-
-
-# ------------------------------------------------------------------------------
-# state(reusing)
-# ------------------------------------------------------------------------------
-# - state(reusing) is set by geturl, ReplayCore
-# - state(reusing) is used by geturl, AsyncTransaction, OpenSocket,
-# ConfigureNewSocket, and ScheduleRequest when creating and configuring the
-# connection.
-# - state(reusing) is used by Connect, Connected, Event x 2 when deciding
-# whether to call TestForReplay.
-# - Other places where state(reusing) is used:
-# - Connected - if reusing and not pipelined, start the state(-timeout)
-# timeout (when writing).
-# - DoneRequest - if reusing and pipelined, send the next pipelined write
-# - Event - if reusing and pipelined, start the state(-timeout)
-# timeout (when reading).
-# - Event - if (not reusing) and pipelined, send the next pipelined
-# write.
-# ------------------------------------------------------------------------------
-
-
-# ------------------------------------------------------------------------------
-# Proc http::AsyncTransaction
-# ------------------------------------------------------------------------------
-# This command is called by geturl and ReplayCore to prepare the HTTP
-# transaction prescribed by a suitably prepared token.
-#
-# Arguments:
-# token - connection token (name of an array)
-#
-# Return Value: none
-# ------------------------------------------------------------------------------
-
-proc http::AsyncTransaction {token} {
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
-
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- set sock $state(sock)
-
- # See comments above re the start of this timeout in other cases.
- if {(!$state(reusing)) && ($state(-timeout) > 0)} {
- set state(after) [after $state(-timeout) \
- [list http::reset $token timeout]]
- }
-
- if { $state(-keepalive)
- && (![info exists socketMapping($state(socketinfo))])
- } {
- # This code is executed only for the first -keepalive request on a
- # socket. It makes the socket persistent.
- ##Log " PreparePersistentConnection" $token -- $sock -- DO
- set DoLater [PreparePersistentConnection $token]
- } else {
- ##Log " PreparePersistentConnection" $token -- $sock -- SKIP
- set DoLater {-traceread 0 -tracewrite 0}
- }
-
- if {$state(ReusingPlaceholder)} {
- # - This request was added to the socketPhQueue of a persistent
- # connection.
- # - But the connection has not yet been created and is a placeholder;
- # - And the placeholder was created by an earlier request.
- # - When that earlier request calls OpenSocket, its placeholder is
- # replaced with a true socket, and it then executes the equivalent of
- # OpenSocket for any subsequent requests that have
- # $state(ReusingPlaceholder).
- Log >J$tk after idle coro NO - ReusingPlaceholder
- } elseif {$state(alreadyQueued)} {
- # - This request was added to the socketWrQueue and socketPlayCmd
- # of a persistent connection that will close at the end of its current
- # read operation.
- Log >J$tk after idle coro NO - alreadyQueued
- } else {
- Log >J$tk after idle coro YES
- set CoroName ${token}--SocketCoroutine
- set cancel [after idle [list coroutine $CoroName ::http::OpenSocket \
- $token $DoLater]]
- dict set socketCoEvent($state(socketinfo)) $token $cancel
- set state(socketcoro) $cancel
- }
-
- return
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc http::PreparePersistentConnection
-# ------------------------------------------------------------------------------
-# This command is called by AsyncTransaction to initialise a "persistent
-# connection" based upon a socket placeholder. It is called the first time the
-# socket is associated with a "-keepalive" request.
-#
-# Arguments:
-# token - connection token (name of an array)
-#
-# Return Value: - DoLater, a dictionary of boolean values listing unfinished
-# tasks; to be passed to ConfigureNewSocket via OpenSocket.
-# ------------------------------------------------------------------------------
-
-proc http::PreparePersistentConnection {token} {
- variable $token
- upvar 0 $token state
-
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- set DoLater {-traceread 0 -tracewrite 0}
- set socketMapping($state(socketinfo)) $state(sock)
- set socketProxyId($state(socketinfo)) $state(proxyUsed)
- # - The value of state(proxyUsed) was set in http::CreateToken to either
- # "none" or "HttpProxy".
- # - $token is the first transaction to use this placeholder, so there are
- # no other tokens whose (proxyUsed) must be modified.
-
- if {![info exists socketRdState($state(socketinfo))]} {
- set socketRdState($state(socketinfo)) {}
- # set varName ::http::socketRdState($state(socketinfo))
- # trace add variable $varName unset ::http::CancelReadPipeline
- dict set DoLater -traceread 1
- }
- if {![info exists socketWrState($state(socketinfo))]} {
- set socketWrState($state(socketinfo)) {}
- # set varName ::http::socketWrState($state(socketinfo))
- # trace add variable $varName unset ::http::CancelWritePipeline
- dict set DoLater -tracewrite 1
- }
-
- if {$state(-pipeline)} {
- #Log new, init for pipelined, GRANT write access to $token in geturl
- # Also grant premature read access to the socket. This is OK.
- set socketRdState($state(socketinfo)) $token
- set socketWrState($state(socketinfo)) $token
- } else {
- # socketWrState is not used by this non-pipelined transaction.
- # We cannot leave it as "Wready" because the next call to
- # http::geturl with a pipelined transaction would conclude that the
- # socket is available for writing.
- #Log new, init for nonpipeline, GRANT r/w access to $token in geturl
- set socketRdState($state(socketinfo)) $token
- set socketWrState($state(socketinfo)) $token
- }
-
- # Value of socketPhQueue() may have already been set by ReplayCore.
- if {![info exists socketPhQueue($state(sock))]} {
- set socketPhQueue($state(sock)) {}
- }
- set socketRdQueue($state(socketinfo)) {}
- set socketWrQueue($state(socketinfo)) {}
- set socketClosing($state(socketinfo)) 0
- set socketPlayCmd($state(socketinfo)) {ReplayIfClose Wready {} {}}
- set socketCoEvent($state(socketinfo)) {}
- set socketProxyId($state(socketinfo)) {}
-
- return $DoLater
-}
-
-# ------------------------------------------------------------------------------
-# Proc ::http::OpenSocket
-# ------------------------------------------------------------------------------
-# This command is called as a coroutine idletask to start the asynchronous HTTP
-# transaction in most cases. For the exceptions, see the calling code in
-# command AsyncTransaction.
-#
-# Arguments:
-# token - connection token (name of an array)
-# DoLater - dictionary of boolean values listing unfinished tasks
-#
-# Return Value: none
-# ------------------------------------------------------------------------------
-
-proc http::OpenSocket {token DoLater} {
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
-
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- Log >K$tk Start OpenSocket coroutine
-
- if {![info exists state(-keepalive)]} {
- # The request has already been cancelled by the calling script.
- return
- }
-
- set sockOld $state(sock)
-
- dict unset socketCoEvent($state(socketinfo)) $token
- unset -nocomplain state(socketcoro)
-
- if {[catch {
- if {$state(reusing)} {
- # If ($state(reusing)) is true, then we do not need to create a new
- # socket, even if $sockOld is only a placeholder for a socket.
- set sock $sockOld
- } else {
- # set sock in the [catch] below.
- set pre [clock milliseconds]
- ##Log pre socket opened, - token $token
- ##Log $state(openCmd) - token $token
- set sock [namespace eval :: $state(openCmd)]
- set state(sock) $sock
- # Normal return from $state(openCmd) always returns a valid socket.
- # A TLS proxy connection with 407 or other failure from the
- # proxy server raises an error.
-
- # Initialisation of a new socket.
- ##Log post socket opened, - token $token
- ##Log socket opened, now fconfigure - token $token
- set delay [expr {[clock milliseconds] - $pre}]
- if {$delay > 3000} {
- Log socket delay $delay - token $token
- }
- fconfigure $sock -translation {auto crlf} \
- -buffersize $state(-blocksize)
- if {[package vsatisfies [package provide Tcl] 9.0-]} {
- fconfigure $sock -profile tcl8
- }
- ##Log socket opened, DONE fconfigure - token $token
- }
-
- Log "Using $sock for $state(socketinfo) - token $token" \
- [expr {$state(-keepalive)?"keepalive":""}]
-
- # Code above has set state(sock) $sock
- ConfigureNewSocket $token $sockOld $DoLater
- ##Log OpenSocket success $sock - token $token
- } result errdict]} {
- ##Log OpenSocket failed $result - token $token
- # There may be other requests in the socketPhQueue.
- # Prepare socketPlayCmd so that Finish will replay them.
- if { ($state(-keepalive)) && (!$state(reusing))
- && [info exists socketPhQueue($sockOld)]
- && ($socketPhQueue($sockOld) ne {})
- } {
- if {$socketMapping($state(socketinfo)) ne $sockOld} {
- Log "WARNING: this code should not be reached.\
- {$socketMapping($state(socketinfo)) ne $sockOld}"
- }
- set socketPlayCmd($state(socketinfo)) [list ReplayIfClose Wready {} $socketPhQueue($sockOld)]
- set socketPhQueue($sockOld) {}
- }
- if {[string range $result 0 20] eq {proxy connect failed:}} {
- # - The HTTPS proxy did not create a socket. The pre-existing value
- # (a "placeholder socket") is unchanged.
- # - The proxy returned a valid HTTP response to the failed CONNECT
- # request, and http::SecureProxyConnect copied this to $token,
- # and also set ${token}(connection) set to "close".
- # - Remove the error message $result so that Finish delivers this
- # HTTP response to the caller.
- set result {}
- }
- Finish $token $result
- # Because socket creation failed, the placeholder "socket" must be
- # "closed" and (if persistent) removed from the persistent sockets
- # table. In the {proxy connect failed:} case Finish does this because
- # the value of ${token}(connection) is "close". In the other cases here,
- # it does so because $result is non-empty.
- }
- ##Log Leaving http::OpenSocket coroutine [info coroutine] - token $token
- return
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc ::http::ConfigureNewSocket
-# ------------------------------------------------------------------------------
-# Command to initialise a newly-created socket. Called only from OpenSocket.
-#
-# This command is called by OpenSocket whenever a genuine socket (sockNew) has
-# been opened for for use by HTTP. It does two things:
-# (1) If $token uses a placeholder socket, this command replaces the placeholder
-# socket with the real socket, not only in $token but in all other requests
-# that use the same placeholder.
-# (2) It calls ScheduleRequest to schedule each request that uses the socket.
-#
-#
-# Value of sockOld/sockNew can be "sock" (genuine socket) or "ph" (placeholder).
-# sockNew is ${token}(sock)
-# sockOld sockNew CASES
-# sock sock (if $reusing, and sockOld is sock)
-# ph sock (if (not $reusing), and sockOld is ph)
-# ph ph (if $reusing, and sockOld is ph) - not called in this case
-# sock ph (cannot occur unless a bug) - not called in this case
-# (if (not $reusing), and sockOld is sock) - illogical
-#
-# Arguments:
-# token - connection token (name of an array)
-# sockOld - handle or placeholder used for a socket before the call to
-# OpenSocket
-# DoLater - dictionary of boolean values listing unfinished tasks
-#
-# Return Value: none
-# ------------------------------------------------------------------------------
-
-proc http::ConfigureNewSocket {token sockOld DoLater} {
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
-
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- set reusing $state(reusing)
- set sock $state(sock)
- set proxyUsed $state(proxyUsed)
- ##Log " ConfigureNewSocket" $token $sockOld ... -- $reusing $sock $proxyUsed
-
- if {(!$reusing) && ($sock ne $sockOld)} {
- # Replace the placeholder value sockOld with sock.
-
- if { [info exists socketMapping($state(socketinfo))]
- && ($socketMapping($state(socketinfo)) eq $sockOld)
- } {
- set socketMapping($state(socketinfo)) $sock
- set socketProxyId($state(socketinfo)) $proxyUsed
- # tokens that use the placeholder $sockOld are updated below.
- ##Log set socketMapping($state(socketinfo)) $sock
- }
-
- # Now finish any tasks left over from PreparePersistentConnection on
- # the connection.
- #
- # The "unset" traces are fired by init (clears entire arrays), and
- # by http::Unset.
- # Unset is called by CloseQueuedQueries and (possibly never) by geturl.
- #
- # CancelReadPipeline, CancelWritePipeline call http::Finish for each
- # token.
- #
- # FIXME If Finish is placeholder-aware, these traces can be set earlier,
- # in PreparePersistentConnection.
-
- if {[dict get $DoLater -traceread]} {
- set varName ::http::socketRdState($state(socketinfo))
- trace add variable $varName unset ::http::CancelReadPipeline
- }
- if {[dict get $DoLater -tracewrite]} {
- set varName ::http::socketWrState($state(socketinfo))
- trace add variable $varName unset ::http::CancelWritePipeline
- }
- }
-
- # Do this in all cases.
- ScheduleRequest $token
-
- # Now look at all other tokens that use the placeholder $sockOld.
- if { (!$reusing)
- && ($sock ne $sockOld)
- && [info exists socketPhQueue($sockOld)]
- } {
- ##Log " ConfigureNewSocket" $token scheduled, now do $socketPhQueue($sockOld)
- foreach tok $socketPhQueue($sockOld) {
- # 1. Amend the token's (sock).
- ##Log set ${tok}(sock) $sock
- set ${tok}(sock) $sock
- set ${tok}(proxyUsed) $proxyUsed
-
- # 2. Schedule the token's HTTP request.
- # Every token in socketPhQueue(*) has reusing 1 alreadyQueued 0.
- set ${tok}(reusing) 1
- set ${tok}(alreadyQueued) 0
- ScheduleRequest $tok
- }
- set socketPhQueue($sockOld) {}
- }
- ##Log " ConfigureNewSocket" $token DONE
-
- return
-}
-
-
-# ------------------------------------------------------------------------------
-# The values of array variables socketMapping etc.
-# ------------------------------------------------------------------------------
-# connId "$host:$port"
-# socketMapping($connId) the handle or placeholder for the socket that is used
-# for "-keepalive 1" requests to $connId.
-# socketRdState($connId) the token that is currently reading from the socket.
-# Other values: Rready (ready for next token to read).
-# socketWrState($connId) the token that is currently writing to the socket.
-# Other values: Wready (ready for next token to write),
-# peNding (would be ready for next write, except that
-# the integrity of a non-pipelined transaction requires
-# waiting until the read(s) in progress are finished).
-# socketRdQueue($connId) List of tokens that are queued for reading later.
-# socketWrQueue($connId) List of tokens that are queued for writing later.
-# socketPhQueue($sock) List of tokens that are queued to use a placeholder
-# socket, when the real socket has not yet been created.
-# socketClosing($connId) (boolean) true iff a server response header indicates
-# that the server will close the connection at the end of
-# the current response.
-# socketPlayCmd($connId) The command to execute to replay pending and
-# part-completed transactions if the socket closes early.
-# socketCoEvent($connId) Identifier for the "after idle" event that will launch
-# an OpenSocket coroutine to open or re-use a socket.
-# socketProxyId($connId) The type of proxy that this socket uses: values are
-# those of state(proxyUsed) i.e. none, HttpProxy,
-# SecureProxy, and SecureProxyFailed.
-# The value is not used for anything by http, its purpose
-# is to set the value of state() for caller information.
-# ------------------------------------------------------------------------------
-
-
-# ------------------------------------------------------------------------------
-# Using socketWrState(*), socketWrQueue(*), socketRdState(*), socketRdQueue(*)
-# ------------------------------------------------------------------------------
-# The element socketWrState($connId) has a value which is either the name of
-# the token that is permitted to write to the socket, or "Wready" if no
-# token is permitted to write.
-#
-# The code that sets the value to Wready immediately calls
-# http::NextPipelinedWrite, which examines socketWrQueue($connId) and
-# processes the next request in the queue, if there is one. The value
-# Wready is not found when the interpreter is in the event loop unless the
-# socket is idle.
-#
-# The element socketRdState($connId) has a value which is either the name of
-# the token that is permitted to read from the socket, or "Rready" if no
-# token is permitted to read.
-#
-# The code that sets the value to Rready then examines
-# socketRdQueue($connId) and processes the next request in the queue, if
-# there is one. The value Rready is not found when the interpreter is in
-# the event loop unless the socket is idle.
-# ------------------------------------------------------------------------------
-
-
-# ------------------------------------------------------------------------------
-# Proc http::ScheduleRequest
-# ------------------------------------------------------------------------------
-# Command to either begin the HTTP request, or add it to the appropriate queue.
-# Called from two places in ConfigureNewSocket.
-#
-# Arguments:
-# token - connection token (name of an array)
-#
-# Return Value: none
-# ------------------------------------------------------------------------------
-
-proc http::ScheduleRequest {token} {
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
-
- Log >L$tk ScheduleRequest
-
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- set Unfinished 0
-
- set reusing $state(reusing)
- set sockNew $state(sock)
-
- # The "if" tests below: must test against the current values of
- # socketWrState, socketRdState, and so the tests must be done here,
- # not earlier in PreparePersistentConnection.
-
- if {$state(alreadyQueued)} {
- # The request has been appended to the queue of a persistent socket
- # (that is scheduled to close and have its queue replayed).
- #
- # A write may or may not be in progress. There is no need to set
- # socketWrState to prevent another call stealing write access - all
- # subsequent calls on this socket will come here because the socket
- # will close after the current read, and its
- # socketClosing($connId) is 1.
- ##Log "HTTP request for token $token is queued"
-
- } elseif { $reusing
- && $state(-pipeline)
- && ($socketWrState($state(socketinfo)) ne "Wready")
- } {
- ##Log "HTTP request for token $token is queued for pipelined use"
- lappend socketWrQueue($state(socketinfo)) $token
-
- } elseif { $reusing
- && (!$state(-pipeline))
- && ($socketWrState($state(socketinfo)) ne "Wready")
- } {
- # A write is queued or in progress. Lappend to the write queue.
- ##Log "HTTP request for token $token is queued for nonpipeline use"
- lappend socketWrQueue($state(socketinfo)) $token
-
- } elseif { $reusing
- && (!$state(-pipeline))
- && ($socketWrState($state(socketinfo)) eq "Wready")
- && ($socketRdState($state(socketinfo)) ne "Rready")
- } {
- # A read is queued or in progress, but not a write. Cannot start the
- # nonpipeline transaction, but must set socketWrState to prevent a
- # pipelined request jumping the queue.
- ##Log "HTTP request for token $token is queued for nonpipeline use"
- #Log re-use nonpipeline, GRANT delayed write access to $token in geturl
- set socketWrState($state(socketinfo)) peNding
- lappend socketWrQueue($state(socketinfo)) $token
-
- } else {
- if {$reusing && $state(-pipeline)} {
- #Log new, init for pipelined, GRANT write access to $token in geturl
- # DO NOT grant premature read access to the socket.
- # set socketRdState($state(socketinfo)) $token
- set socketWrState($state(socketinfo)) $token
- } elseif {$reusing} {
- # socketWrState is not used by this non-pipelined transaction.
- # We cannot leave it as "Wready" because the next call to
- # http::geturl with a pipelined transaction would conclude that the
- # socket is available for writing.
- #Log new, init for nonpipeline, GRANT r/w access to $token in geturl
- set socketRdState($state(socketinfo)) $token
- set socketWrState($state(socketinfo)) $token
- } else {
- }
-
- # Process the request now.
- # - Command is not called unless $state(sock) is a real socket handle
- # and not a placeholder.
- # - All (!$reusing) cases come here.
- # - Some $reusing cases come here too if the connection is
- # marked as ready. Those $reusing cases are:
- # $reusing && ($socketWrState($state(socketinfo)) eq "Wready") &&
- # EITHER !$pipeline && ($socketRdState($state(socketinfo)) eq "Rready")
- # OR $pipeline
- #
- #Log ---- $state(socketinfo) << conn to $token for HTTP request (a)
- ##Log " ScheduleRequest" $token -- fileevent $state(sock) writable for $token
- # Connect does its own fconfigure.
-
- lassign $state(connArgs) proto phost srvurl
-
- if {[catch {
- fileevent $state(sock) writable \
- [list http::Connect $token $proto $phost $srvurl]
- } res opts]} {
- # The socket no longer exists.
- ##Log bug -- socket gone -- $res -- $opts
- }
-
- }
-
- return
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc http::SendHeader
-# ------------------------------------------------------------------------------
-# Command to send a request header, and keep a copy in state(requestHeaders)
-# for debugging purposes.
-#
-# Arguments:
-# token - connection token (name of an array)
-# key - header name
-# value - header value
-#
-# Return Value: none
-# ------------------------------------------------------------------------------
-
-proc http::SendHeader {token key value} {
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
- set sock $state(sock)
- lappend state(requestHeaders) [string tolower $key] $value
- puts $sock "$key: $value"
- return
-}
-
-# http::Connected --
-#
-# Callback used when the connection to the HTTP server is actually
-# established.
-#
-# Arguments:
-# token State token.
-# proto What protocol (http, https, etc.) was used to connect.
-# phost Are we using keep-alive? Non-empty if yes.
-# srvurl Service-local URL that we're requesting
-# Results:
-# None.
-
-proc http::Connected {token proto phost srvurl} {
- variable http
- variable urlTypes
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
-
- if {$state(reusing) && (!$state(-pipeline)) && ($state(-timeout) > 0)} {
- set state(after) [after $state(-timeout) \
- [list http::reset $token timeout]]
- }
-
- # Set back the variables needed here.
- set sock $state(sock)
- set isQueryChannel [info exists state(-querychannel)]
- set isQuery [info exists state(-query)]
- regexp {^(.+):([^:]+)$} $state(socketinfo) {} host port
-
- set lower [string tolower $proto]
- set defport [lindex $urlTypes($lower) 0]
-
- # Send data in cr-lf format, but accept any line terminators.
- # Initialisation to {auto *} now done in geturl, KeepSocket and DoneRequest.
- # We are concerned here with the request (write) not the response (read).
- lassign [fconfigure $sock -translation] trRead trWrite
- fconfigure $sock -translation [list $trRead crlf] \
- -buffersize $state(-blocksize)
- if {[package vsatisfies [package provide Tcl] 9.0-]} {
- fconfigure $sock -profile tcl8
- }
-
- # The following is disallowed in safe interpreters, but the socket is
- # already in non-blocking mode in that case.
-
- catch {fconfigure $sock -blocking off}
- set how GET
- if {$isQuery} {
- set state(querylength) [string length $state(-query)]
- if {$state(querylength) > 0} {
- set how POST
- set contDone 0
- } else {
- # There's no query data.
- unset state(-query)
- set isQuery 0
- }
- } elseif {$state(-validate)} {
- set how HEAD
- } elseif {$isQueryChannel} {
- set how POST
- # The query channel must be blocking for the async Write to
- # work properly.
- fconfigure $state(-querychannel) -blocking 1 -translation binary
- set contDone 0
- }
- if {[info exists state(-method)] && ($state(-method) ne "")} {
- set how $state(-method)
- }
- set accept_types_seen 0
-
- Log ^B$tk begin sending request - token $token
-
- if {[catch {
- if {[info exists state(bypass)]} {
- set state(method) [lindex [split $state(bypass) { }] 0]
- set state(requestHeaders) {}
- set state(requestLine) $state(bypass)
- } else {
- set state(method) $how
- set state(requestHeaders) {}
- set state(requestLine) "$how $srvurl HTTP/$state(-protocol)"
- }
- puts $sock $state(requestLine)
- set hostValue [GetFieldValue $state(-headers) Host]
- if {$hostValue ne {}} {
- # Allow Host spoofing. [Bug 928154]
- regexp {^[^:]+} $hostValue state(host)
- SendHeader $token Host $hostValue
- } elseif {$port == $defport} {
- # Don't add port in this case, to handle broken servers. [Bug
- # #504508]
- set state(host) $host
- SendHeader $token Host $host
- } else {
- set state(host) $host
- SendHeader $token Host "$host:$port"
- }
- SendHeader $token User-Agent $http(-useragent)
- if {($state(-protocol) > 1.0) && $state(-keepalive)} {
- # Send this header, because a 1.1 server is not compelled to treat
- # this as the default.
- set ConnVal keep-alive
- } elseif {($state(-protocol) > 1.0)} {
- # RFC2616 sec 8.1.2.1
- set ConnVal close
- } else {
- # ($state(-protocol) <= 1.0)
- # RFC7230 A.1
- # Some server implementations of HTTP/1.0 have a faulty
- # implementation of RFC 2068 Keep-Alive.
- # Don't leave this to chance.
- # For HTTP/1.0 we have already "set state(connection) close"
- # and "state(-keepalive) 0".
- set ConnVal close
- }
- # Proxy authorisation (cf. mod by Anders Ramdahl to autoproxy by
- # Pat Thoyts).
- if {($http(-proxyauth) ne {}) && ($state(proxyUsed) eq {HttpProxy})} {
- SendHeader $token Proxy-Authorization $http(-proxyauth)
- }
- # RFC7230 A.1 - "clients are encouraged not to send the
- # Proxy-Connection header field in any requests"
- set accept_encoding_seen 0
- set content_type_seen 0
- set connection_seen 0
- foreach {key value} $state(-headers) {
- set value [string map [list \n "" \r ""] $value]
- set key [string map {" " -} [string trim $key]]
- if {[string equal -nocase $key "host"]} {
- continue
- }
- if {[string equal -nocase $key "accept-encoding"]} {
- set accept_encoding_seen 1
- }
- if {[string equal -nocase $key "accept"]} {
- set accept_types_seen 1
- }
- if {[string equal -nocase $key "content-type"]} {
- set content_type_seen 1
- }
- if {[string equal -nocase $key "content-length"]} {
- set contDone 1
- set state(querylength) $value
- }
- if { [string equal -nocase $key "connection"]
- && [info exists state(bypass)]
- } {
- # Value supplied in -headers overrides $ConnVal.
- set connection_seen 1
- } elseif {[string equal -nocase $key "connection"]} {
- # Remove "close" or "keep-alive" and use our own value.
- # In an upgrade request, the upgrade is not guaranteed.
- # Value "close" or "keep-alive" tells the server what to do
- # if it refuses the upgrade. We send a single "Connection"
- # header because some websocket servers, e.g. civetweb, reject
- # multiple headers. Bug [d01de3281f] of tcllib/websocket.
- set connection_seen 1
- set listVal $state(connectionValues)
- if {[set pos [lsearch $listVal close]] != -1} {
- set listVal [lreplace $listVal $pos $pos]
- }
- if {[set pos [lsearch $listVal keep-alive]] != -1} {
- set listVal [lreplace $listVal $pos $pos]
- }
- lappend listVal $ConnVal
- set value [join $listVal {, }]
- }
- if {[string length $key]} {
- SendHeader $token $key $value
- }
- }
- # Allow overriding the Accept header on a per-connection basis. Useful
- # for working with REST services. [Bug c11a51c482]
- if {!$accept_types_seen} {
- SendHeader $token Accept $state(accept-types)
- }
- if { (!$accept_encoding_seen)
- && (![info exists state(-handler)])
- && $http(-zip)
- } {
- SendHeader $token Accept-Encoding gzip,deflate
- } elseif {!$accept_encoding_seen} {
- SendHeader $token Accept-Encoding identity
- } else {
- }
- if {!$connection_seen} {
- SendHeader $token Connection $ConnVal
- }
- if {$isQueryChannel && ($state(querylength) == 0)} {
- # Try to determine size of data in channel. If we cannot seek, the
- # surrounding catch will trap us
-
- set start [tell $state(-querychannel)]
- seek $state(-querychannel) 0 end
- set state(querylength) \
- [expr {[tell $state(-querychannel)] - $start}]
- seek $state(-querychannel) $start
- }
-
- # Note that we don't do Cookie2; that's much nastier and not normally
- # observed in practice either. It also doesn't fix the multitude of
- # bugs in the basic cookie spec.
- if {$http(-cookiejar) ne ""} {
- set cookies ""
- set separator ""
- foreach {key value} [{*}$http(-cookiejar) \
- getCookies $proto $host $state(path)] {
- append cookies $separator $key = $value
- set separator "; "
- }
- if {$cookies ne ""} {
- SendHeader $token Cookie $cookies
- }
- }
-
- # Flush the request header and set up the fileevent that will either
- # push the POST data or read the response.
- #
- # fileevent note:
- #
- # It is possible to have both the read and write fileevents active at
- # this point. The only scenario it seems to affect is a server that
- # closes the connection without reading the POST data. (e.g., early
- # versions TclHttpd in various error cases). Depending on the
- # platform, the client may or may not be able to get the response from
- # the server because of the error it will get trying to write the post
- # data. Having both fileevents active changes the timing and the
- # behavior, but no two platforms (among Solaris, Linux, and NT) behave
- # the same, and none behave all that well in any case. Servers should
- # always read their POST data if they expect the client to read their
- # response.
-
- if {$isQuery || $isQueryChannel} {
- # POST method.
- if {!$content_type_seen} {
- SendHeader $token Content-Type $state(-type)
- }
- if {!$contDone} {
- SendHeader $token Content-Length $state(querylength)
- }
- puts $sock ""
- flush $sock
- # Flush flushes the error in the https case with a bad handshake:
- # else the socket never becomes writable again, and hangs until
- # timeout (if any).
-
- lassign [fconfigure $sock -translation] trRead trWrite
- fconfigure $sock -translation [list $trRead binary]
- fileevent $sock writable [list http::Write $token]
- # The http::Write command decides when to make the socket readable,
- # using the same test as the GET/HEAD case below.
- } else {
- # GET or HEAD method.
- if { (![catch {fileevent $sock readable} binding])
- && ($binding eq [list http::CheckEof $sock])
- } {
- # Remove the "fileevent readable" binding of an idle persistent
- # socket to http::CheckEof. We can no longer treat bytes
- # received as junk. The server might still time out and
- # half-close the socket if it has not yet received the first
- # "puts".
- fileevent $sock readable {}
- }
- puts $sock ""
- flush $sock
- Log ^C$tk end sending request - token $token
- # End of writing (GET/HEAD methods). The request has been sent.
-
- DoneRequest $token
- }
-
- } err]} {
- # The socket probably was never connected, OR the connection dropped
- # later, OR https handshake error, which may be discovered as late as
- # the "flush" command above...
- Log "WARNING - if testing, pay special attention to this\
- case (GI) which is seldom executed - token $token"
- if {[info exists state(reusing)] && $state(reusing)} {
- # The socket was closed at the server end, and closed at
- # this end by http::CheckEof.
- if {[TestForReplay $token write $err a]} {
- return
- } else {
- Finish $token {failed to re-use socket}
- }
-
- # else:
- # This is NOT a persistent socket that has been closed since its
- # last use.
- # If any other requests are in flight or pipelined/queued, they will
- # be discarded.
- } elseif {$state(status) eq ""} {
- # https handshake errors come here, for
- # Tcl 8.7 without http::SecureProxyConnect, and for Tcl 8.6.
- set msg [registerError $sock]
- registerError $sock {}
- if {$msg eq {}} {
- set msg {failed to use socket}
- }
- Finish $token $msg
- } elseif {$state(status) ne "error"} {
- Finish $token $err
- }
- }
- return
-}
-
-# http::registerError
-#
-# Called (for example when processing TclTLS activity) to register
-# an error for a connection on a specific socket. This helps
-# http::Connected to deliver meaningful error messages, e.g. when a TLS
-# certificate fails verification.
-#
-# Usage: http::registerError socket ?newValue?
-#
-# "set" semantics, except that a "get" (a call without a new value) for a
-# non-existent socket returns {}, not an error.
-
-proc http::registerError {sock args} {
- variable registeredErrors
-
- if { ([llength $args] == 0)
- && (![info exists registeredErrors($sock)])
- } {
- return
- } elseif { ([llength $args] == 1)
- && ([lindex $args 0] eq {})
- } {
- unset -nocomplain registeredErrors($sock)
- return
- }
- set registeredErrors($sock) {*}$args
-}
-
-# http::DoneRequest --
-#
-# Command called when a request has been sent. It will arrange the
-# next request and/or response as appropriate.
-#
-# If this command is called when $socketClosing(*), the request $token
-# that calls it must be pipelined and destined to fail.
-
-proc http::DoneRequest {token} {
- variable http
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
- set sock $state(sock)
-
- # If pipelined, connect the next HTTP request to the socket.
- if {$state(reusing) && $state(-pipeline)} {
- # Enable next token (if any) to write.
- # The value "Wready" is set only here, and
- # in http::Event after reading the response-headers of a
- # non-reusing transaction.
- # Previous value is $token. It cannot be pending.
- set socketWrState($state(socketinfo)) Wready
-
- # Now ready to write the next pipelined request (if any).
- http::NextPipelinedWrite $token
- } else {
- # If pipelined, this is the first transaction on this socket. We wait
- # for the response headers to discover whether the connection is
- # persistent. (If this is not done and the connection is not
- # persistent, we SHOULD retry and then MUST NOT pipeline before knowing
- # that we have a persistent connection
- # (rfc2616 8.1.2.2)).
- }
-
- # Connect to receive the response, unless the socket is pipelined
- # and another response is being sent.
- # This code block is separate from the code below because there are
- # cases where socketRdState already has the value $token.
- if { $state(-keepalive)
- && $state(-pipeline)
- && [info exists socketRdState($state(socketinfo))]
- && ($socketRdState($state(socketinfo)) eq "Rready")
- } {
- #Log pipelined, GRANT read access to $token in Connected
- set socketRdState($state(socketinfo)) $token
- }
-
- if { $state(-keepalive)
- && $state(-pipeline)
- && [info exists socketRdState($state(socketinfo))]
- && ($socketRdState($state(socketinfo)) ne $token)
- } {
- # Do not read from the socket until it is ready.
- ##Log "HTTP response for token $token is queued for pipelined use"
- # If $socketClosing(*), then the caller will be a pipelined write and
- # execution will come here.
- # This token has already been recorded as "in flight" for writing.
- # When the socket is closed, the read queue will be cleared in
- # CloseQueuedQueries and so the "lappend" here has no effect.
- lappend socketRdQueue($state(socketinfo)) $token
- } else {
- # In the pipelined case, connection for reading depends on the
- # value of socketRdState.
- # In the nonpipeline case, connection for reading always occurs.
- ReceiveResponse $token
- }
- return
-}
-
-# http::ReceiveResponse
-#
-# Connects token to its socket for reading.
-
-proc http::ReceiveResponse {token} {
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
- set sock $state(sock)
-
- #Log ---- $state(socketinfo) >> conn to $token for HTTP response
- lassign [fconfigure $sock -translation] trRead trWrite
- fconfigure $sock -translation [list auto $trWrite] \
- -buffersize $state(-blocksize)
- if {[package vsatisfies [package provide Tcl] 9.0-]} {
- fconfigure $sock -profile tcl8
- }
- Log ^D$tk begin receiving response - token $token
-
- coroutine ${token}--EventCoroutine http::Event $sock $token
- if {[info exists state(-handler)] || [info exists state(-progress)]} {
- fileevent $sock readable [list http::EventGateway $sock $token]
- } else {
- fileevent $sock readable ${token}--EventCoroutine
- }
- return
-}
-
-
-# http::EventGateway
-#
-# Bug [c2dc1da315].
-# - Recursive launch of the coroutine can occur if a -handler or -progress
-# callback is used, and the callback command enters the event loop.
-# - To prevent this, the fileevent "binding" is disabled while the
-# coroutine is in flight.
-# - If a recursive call occurs despite these precautions, it is not
-# trapped and discarded here, because it is better to report it as a
-# bug.
-# - Although this solution is believed to be sufficiently general, it is
-# used only if -handler or -progress is specified. In other cases,
-# the coroutine is called directly.
-
-proc http::EventGateway {sock token} {
- variable $token
- upvar 0 $token state
- fileevent $sock readable {}
- catch {${token}--EventCoroutine} res opts
- if {[info commands ${token}--EventCoroutine] ne {}} {
- # The coroutine can be deleted by completion (a non-yield return), by
- # http::Finish (when there is a premature end to the transaction), by
- # http::reset or http::cleanup, or if the caller set option -channel
- # but not option -handler: in the last case reading from the socket is
- # now managed by commands ::http::Copy*, http::ReceiveChunked, and
- # http::MakeTransformationChunked.
- #
- # Catch in case the coroutine has closed the socket.
- catch {fileevent $sock readable [list http::EventGateway $sock $token]}
- }
-
- # If there was an error, re-throw it.
- return -options $opts $res
-}
-
-
-# http::NextPipelinedWrite
-#
-# - Connecting a socket to a token for writing is done by this command and by
-# command KeepSocket.
-# - If another request has a pipelined write scheduled for $token's socket,
-# and if the socket is ready to accept it, connect the write and update
-# the queue accordingly.
-# - This command is called from http::DoneRequest and http::Event,
-# IF $state(-pipeline) AND (the current transfer has reached the point at
-# which the socket is ready for the next request to be written).
-# - This command is called when a token has write access and is pipelined and
-# keep-alive, and sets socketWrState to Wready.
-# - The command need not consider the case where socketWrState is set to a token
-# that does not yet have write access. Such a token is waiting for Rready,
-# and the assignment of the connection to the token will be done elsewhere (in
-# http::KeepSocket).
-# - This command cannot be called after socketWrState has been set to a
-# "pending" token value (that is then overwritten by the caller), because that
-# value is set by this command when it is called by an earlier token when it
-# relinquishes its write access, and the pending token is always the next in
-# line to write.
-
-proc http::NextPipelinedWrite {token} {
- variable http
- variable socketRdState
- variable socketWrState
- variable socketWrQueue
- variable socketClosing
- variable $token
- upvar 0 $token state
- set connId $state(socketinfo)
-
- if { [info exists socketClosing($connId)]
- && $socketClosing($connId)
- } {
- # socketClosing(*) is set because the server has sent a
- # "Connection: close" header.
- # Behave as if the queues are empty - so do nothing.
- } elseif { $state(-pipeline)
- && [info exists socketWrState($connId)]
- && ($socketWrState($connId) eq "Wready")
-
- && [info exists socketWrQueue($connId)]
- && [llength $socketWrQueue($connId)]
- && ([set token2 [lindex $socketWrQueue($connId) 0]
- set ${token2}(-pipeline)
- ]
- )
- } {
- # - The usual case for a pipelined connection, ready for a new request.
- #Log pipelined, GRANT write access to $token2 in NextPipelinedWrite
- set conn [set ${token2}(connArgs)]
- set socketWrState($connId) $token2
- set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end]
- # Connect does its own fconfigure.
- fileevent $state(sock) writable [list http::Connect $token2 {*}$conn]
- #Log ---- $connId << conn to $token2 for HTTP request (b)
-
- # In the tests below, the next request will be nonpipeline.
- } elseif { $state(-pipeline)
- && [info exists socketWrState($connId)]
- && ($socketWrState($connId) eq "Wready")
-
- && [info exists socketWrQueue($connId)]
- && [llength $socketWrQueue($connId)]
- && (![ set token3 [lindex $socketWrQueue($connId) 0]
- set ${token3}(-pipeline)
- ]
- )
-
- && [info exists socketRdState($connId)]
- && ($socketRdState($connId) eq "Rready")
- } {
- # The case in which the next request will be non-pipelined, and the read
- # and write queues is ready: which is the condition for a non-pipelined
- # write.
- set conn [set ${token3}(connArgs)]
- #Log nonpipeline, GRANT r/w access to $token3 in NextPipelinedWrite
- set socketRdState($connId) $token3
- set socketWrState($connId) $token3
- set socketWrQueue($connId) [lrange $socketWrQueue($connId) 1 end]
- # Connect does its own fconfigure.
- fileevent $state(sock) writable [list http::Connect $token3 {*}$conn]
- #Log ---- $state(sock) << conn to $token3 for HTTP request (c)
-
- } elseif { $state(-pipeline)
- && [info exists socketWrState($connId)]
- && ($socketWrState($connId) eq "Wready")
-
- && [info exists socketWrQueue($connId)]
- && [llength $socketWrQueue($connId)]
- && (![set token2 [lindex $socketWrQueue($connId) 0]
- set ${token2}(-pipeline)
- ]
- )
- } {
- # - The case in which the next request will be non-pipelined, but the
- # read queue is NOT ready.
- # - A read is queued or in progress, but not a write. Cannot start the
- # nonpipeline transaction, but must set socketWrState to prevent a new
- # pipelined request (in http::geturl) jumping the queue.
- # - Because socketWrState($connId) is not set to Wready, the assignment
- # of the connection to $token2 will be done elsewhere - by command
- # http::KeepSocket when $socketRdState($connId) is set to "Rready".
-
- #Log re-use nonpipeline, GRANT delayed write access to $token in NextP..
- set socketWrState($connId) peNding
- }
- return
-}
-
-# http::CancelReadPipeline
-#
-# Cancel pipelined responses on a closing "Keep-Alive" socket.
-#
-# - Called by a variable trace on "unset socketRdState($connId)".
-# - The variable relates to a Keep-Alive socket, which has been closed.
-# - Cancels all pipelined responses. The requests have been sent,
-# the responses have not yet been received.
-# - This is a hard cancel that ends each transaction with error status,
-# and closes the connection. Do not use it if you want to replay failed
-# transactions.
-# - N.B. Always delete ::http::socketRdState($connId) before deleting
-# ::http::socketRdQueue($connId), or this command will do nothing.
-#
-# Arguments
-# As for a trace command on a variable.
-
-proc http::CancelReadPipeline {name1 connId op} {
- variable socketRdQueue
- ##Log CancelReadPipeline $name1 $connId $op
- if {[info exists socketRdQueue($connId)]} {
- set msg {the connection was closed by CancelReadPipeline}
- foreach token $socketRdQueue($connId) {
- set tk [namespace tail $token]
- Log ^X$tk end of response "($msg)" - token $token
- set ${token}(status) eof
- Finish $token ;#$msg
- }
- set socketRdQueue($connId) {}
- }
- return
-}
-
-# http::CancelWritePipeline
-#
-# Cancel queued events on a closing "Keep-Alive" socket.
-#
-# - Called by a variable trace on "unset socketWrState($connId)".
-# - The variable relates to a Keep-Alive socket, which has been closed.
-# - In pipelined or nonpipeline case: cancels all queued requests. The
-# requests have not yet been sent, the responses are not due.
-# - This is a hard cancel that ends each transaction with error status,
-# and closes the connection. Do not use it if you want to replay failed
-# transactions.
-# - N.B. Always delete ::http::socketWrState($connId) before deleting
-# ::http::socketWrQueue($connId), or this command will do nothing.
-#
-# Arguments
-# As for a trace command on a variable.
-
-proc http::CancelWritePipeline {name1 connId op} {
- variable socketWrQueue
-
- ##Log CancelWritePipeline $name1 $connId $op
- if {[info exists socketWrQueue($connId)]} {
- set msg {the connection was closed by CancelWritePipeline}
- foreach token $socketWrQueue($connId) {
- set tk [namespace tail $token]
- Log ^X$tk end of response "($msg)" - token $token
- set ${token}(status) eof
- Finish $token ;#$msg
- }
- set socketWrQueue($connId) {}
- }
- return
-}
-
-# http::ReplayIfDead --
-#
-# - A query on a re-used persistent socket failed at the earliest opportunity,
-# because the socket had been closed by the server. Keep the token, tidy up,
-# and try to connect on a fresh socket.
-# - The connection is monitored for eof by the command http::CheckEof. Thus
-# http::ReplayIfDead is needed only when a server event (half-closing an
-# apparently idle connection), and a client event (sending a request) occur at
-# almost the same time, and neither client nor server detects the other's
-# action before performing its own (an "asynchronous close event").
-# - To simplify testing of http::ReplayIfDead, set TEST_EOF 1 in
-# http::KeepSocket, and then http::ReplayIfDead will be called if http::geturl
-# is called at any time after the server timeout.
-#
-# Arguments:
-# token Connection token.
-#
-# Side Effects:
-# Use the same token, but try to open a new socket.
-
-proc http::ReplayIfDead {token doing} {
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- variable $token
- upvar 0 $token state
-
- Log running http::ReplayIfDead for $token $doing
-
- # 1. Merge the tokens for transactions in flight, the read (response) queue,
- # and the write (request) queue.
-
- set InFlightR {}
- set InFlightW {}
-
- # Obtain the tokens for transactions in flight.
- if {$state(-pipeline)} {
- # Two transactions may be in flight. The "read" transaction was first.
- # It is unlikely that the server would close the socket if a response
- # was pending; however, an earlier request (as well as the present
- # request) may have been sent and ignored if the socket was half-closed
- # by the server.
-
- if { [info exists socketRdState($state(socketinfo))]
- && ($socketRdState($state(socketinfo)) ne "Rready")
- } {
- lappend InFlightR $socketRdState($state(socketinfo))
- } elseif {($doing eq "read")} {
- lappend InFlightR $token
- }
-
- if { [info exists socketWrState($state(socketinfo))]
- && $socketWrState($state(socketinfo)) ni {Wready peNding}
- } {
- lappend InFlightW $socketWrState($state(socketinfo))
- } elseif {($doing eq "write")} {
- lappend InFlightW $token
- }
-
- # Report any inconsistency of $token with socket*state.
- if { ($doing eq "read")
- && [info exists socketRdState($state(socketinfo))]
- && ($token ne $socketRdState($state(socketinfo)))
- } {
- Log WARNING - ReplayIfDead pipelined token $token $doing \
- ne socketRdState($state(socketinfo)) \
- $socketRdState($state(socketinfo))
-
- } elseif {
- ($doing eq "write")
- && [info exists socketWrState($state(socketinfo))]
- && ($token ne $socketWrState($state(socketinfo)))
- } {
- Log WARNING - ReplayIfDead pipelined token $token $doing \
- ne socketWrState($state(socketinfo)) \
- $socketWrState($state(socketinfo))
- }
- } else {
- # One transaction should be in flight.
- # socketRdState, socketWrQueue are used.
- # socketRdQueue should be empty.
-
- # Report any inconsistency of $token with socket*state.
- if {$token ne $socketRdState($state(socketinfo))} {
- Log WARNING - ReplayIfDead nonpipeline token $token $doing \
- ne socketRdState($state(socketinfo)) \
- $socketRdState($state(socketinfo))
- }
-
- # Report the inconsistency that socketRdQueue is non-empty.
- if { [info exists socketRdQueue($state(socketinfo))]
- && ($socketRdQueue($state(socketinfo)) ne {})
- } {
- Log WARNING - ReplayIfDead nonpipeline token $token $doing \
- has read queue socketRdQueue($state(socketinfo)) \
- $socketRdQueue($state(socketinfo)) ne {}
- }
-
- lappend InFlightW $socketRdState($state(socketinfo))
- set socketRdQueue($state(socketinfo)) {}
- }
-
- set newQueue {}
- lappend newQueue {*}$InFlightR
- lappend newQueue {*}$socketRdQueue($state(socketinfo))
- lappend newQueue {*}$InFlightW
- lappend newQueue {*}$socketWrQueue($state(socketinfo))
-
-
- # 2. Tidy up token. This is a cut-down form of Finish/CloseSocket.
- # Do not change state(status).
- # No need to after cancel state(after) - either this is done in
- # ReplayCore/ReInit, or Finish is called.
-
- catch {close $state(sock)}
- Unset $state(socketinfo)
-
- # 2a. Tidy the tokens in the queues - this is done in ReplayCore/ReInit.
- # - Transactions, if any, that are awaiting responses cannot be completed.
- # They are listed for re-sending in newQueue.
- # - All tokens are preserved for re-use by ReplayCore, and their variables
- # will be re-initialised by calls to ReInit.
- # - The relevant element of socketMapping, socketRdState, socketWrState,
- # socketRdQueue, socketWrQueue, socketClosing, socketPlayCmd will be set
- # to new values in ReplayCore.
-
- ReplayCore $newQueue
- return
-}
-
-# http::ReplayIfClose --
-#
-# A request on a socket that was previously "Connection: keep-alive" has
-# received a "Connection: close" response header. The server supplies
-# that response correctly, but any later requests already queued on this
-# connection will be lost when the socket closes.
-#
-# This command takes arguments that represent the socketWrState,
-# socketRdQueue and socketWrQueue for this connection. The socketRdState
-# is not needed because the server responds in full to the request that
-# received the "Connection: close" response header.
-#
-# Existing request tokens $token (::http::$n) are preserved. The caller
-# will be unaware that the request was processed this way.
-
-proc http::ReplayIfClose {Wstate Rqueue Wqueue} {
- Log running http::ReplayIfClose for $Wstate $Rqueue $Wqueue
-
- if {$Wstate in $Rqueue || $Wstate in $Wqueue} {
- Log WARNING duplicate token in http::ReplayIfClose - token $Wstate
- set Wstate Wready
- }
-
- # 1. Create newQueue
- set InFlightW {}
- if {$Wstate ni {Wready peNding}} {
- lappend InFlightW $Wstate
- }
- ##Log $Rqueue -- $InFlightW -- $Wqueue
- set newQueue {}
- lappend newQueue {*}$Rqueue
- lappend newQueue {*}$InFlightW
- lappend newQueue {*}$Wqueue
-
- # 2. Cleanup - none needed, done by the caller.
-
- ReplayCore $newQueue
- return
-}
-
-# http::ReInit --
-#
-# Command to restore a token's state to a condition that
-# makes it ready to replay a request.
-#
-# Command http::geturl stores extra state in state(tmp*) so
-# we don't need to do the argument processing again.
-#
-# The caller must:
-# - Set state(reusing) and state(sock) to their new values after calling
-# this command.
-# - Unset state(tmpState), state(tmpOpenCmd) if future calls to ReplayCore
-# or ReInit are inappropriate for this token. Typically only one retry
-# is allowed.
-# The caller may also unset state(tmpConnArgs) if this value (and the
-# token) will be used immediately. The value is needed by tokens that
-# will be stored in a queue.
-#
-# Arguments:
-# token Connection token.
-#
-# Return Value: (boolean) true iff the re-initialisation was successful.
-
-proc http::ReInit {token} {
- variable $token
- upvar 0 $token state
-
- if {!(
- [info exists state(tmpState)]
- && [info exists state(tmpOpenCmd)]
- && [info exists state(tmpConnArgs)]
- )
- } {
- Log FAILED in http::ReInit via ReplayCore - NO tmp vars for $token
- return 0
- }
-
- if {[info exists state(after)]} {
- after cancel $state(after)
- unset state(after)
- }
- if {[info exists state(socketcoro)]} {
- Log $token Cancel socket after-idle event (ReInit)
- after cancel $state(socketcoro)
- unset state(socketcoro)
- }
-
- # Don't alter state(status) - this would trigger http::wait if it is in use.
- set tmpState $state(tmpState)
- set tmpOpenCmd $state(tmpOpenCmd)
- set tmpConnArgs $state(tmpConnArgs)
- foreach name [array names state] {
- if {$name ne "status"} {
- unset state($name)
- }
- }
-
- # Don't alter state(status).
- # Restore state(tmp*) - the caller may decide to unset them.
- # Restore state(tmpConnArgs) which is needed for connection.
- # state(tmpState), state(tmpOpenCmd) are needed only for retries.
-
- dict unset tmpState status
- array set state $tmpState
- set state(tmpState) $tmpState
- set state(tmpOpenCmd) $tmpOpenCmd
- set state(tmpConnArgs) $tmpConnArgs
-
- return 1
-}
-
-# http::ReplayCore --
-#
-# Command to replay a list of requests, using existing connection tokens.
-#
-# Abstracted from http::geturl which stores extra state in state(tmp*) so
-# we don't need to do the argument processing again.
-#
-# Arguments:
-# newQueue List of connection tokens.
-#
-# Side Effects:
-# Use existing tokens, but try to open a new socket.
-
-proc http::ReplayCore {newQueue} {
- variable TmpSockCounter
-
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- if {[llength $newQueue] == 0} {
- # Nothing to do.
- return
- }
-
- ##Log running ReplayCore for {*}$newQueue
- set newToken [lindex $newQueue 0]
- set newQueue [lrange $newQueue 1 end]
-
- # 3. Use newToken, and restore its values of state(*). Do not restore
- # elements tmp* - we try again only once.
-
- set token $newToken
- variable $token
- upvar 0 $token state
-
- if {![ReInit $token]} {
- Log FAILED in http::ReplayCore - NO tmp vars
- Log ReplayCore reject $token
- Finish $token {cannot send this request again}
- return
- }
-
- set tmpState $state(tmpState)
- set tmpOpenCmd $state(tmpOpenCmd)
- set tmpConnArgs $state(tmpConnArgs)
- unset state(tmpState)
- unset state(tmpOpenCmd)
- unset state(tmpConnArgs)
-
- set state(reusing) 0
- set state(ReusingPlaceholder) 0
- set state(alreadyQueued) 0
- Log ReplayCore replay $token
-
- # Give the socket a placeholder name before it is created.
- set sock HTTP_PLACEHOLDER_[incr TmpSockCounter]
- set state(sock) $sock
-
- # Move the $newQueue into the placeholder socket's socketPhQueue.
- set socketPhQueue($sock) {}
- foreach tok $newQueue {
- if {[ReInit $tok]} {
- set ${tok}(reusing) 1
- set ${tok}(sock) $sock
- lappend socketPhQueue($sock) $tok
- Log ReplayCore replay $tok
- } else {
- Log ReplayCore reject $tok
- set ${tok}(reusing) 1
- set ${tok}(sock) NONE
- Finish $tok {cannot send this request again}
- }
- }
-
- AsyncTransaction $token
-
- return
-}
-
-# Data access functions:
-# Data - the URL data
-# Status - the transaction status: ok, reset, eof, timeout, error
-# Code - the HTTP transaction code, e.g., 200
-# Size - the size of the URL data
-
-proc http::responseBody {token} {
- variable $token
- upvar 0 $token state
- return $state(body)
-}
-proc http::status {token} {
- if {![info exists $token]} {
- return "error"
- }
- variable $token
- upvar 0 $token state
- return $state(status)
-}
-proc http::responseLine {token} {
- variable $token
- upvar 0 $token state
- return $state(http)
-}
-proc http::requestLine {token} {
- variable $token
- upvar 0 $token state
- return $state(requestLine)
-}
-proc http::responseCode {token} {
- variable $token
- upvar 0 $token state
- if {[regexp {[0-9]{3}} $state(http) numeric_code]} {
- return $numeric_code
- } else {
- return $state(http)
- }
-}
-proc http::size {token} {
- variable $token
- upvar 0 $token state
- return $state(currentsize)
-}
-proc http::requestHeaders {token args} {
- set lenny [llength $args]
- if {$lenny > 1} {
- return -code error {usage: ::http::requestHeaders token ?headerName?}
- } else {
- return [Meta $token request {*}$args]
- }
-}
-proc http::responseHeaders {token args} {
- set lenny [llength $args]
- if {$lenny > 1} {
- return -code error {usage: ::http::responseHeaders token ?headerName?}
- } else {
- return [Meta $token response {*}$args]
- }
-}
-proc http::requestHeaderValue {token header} {
- Meta $token request $header VALUE
-}
-proc http::responseHeaderValue {token header} {
- Meta $token response $header VALUE
-}
-proc http::Meta {token who args} {
- variable $token
- upvar 0 $token state
-
- if {$who eq {request}} {
- set whom requestHeaders
- } elseif {$who eq {response}} {
- set whom meta
- } else {
- return -code error {usage: ::http::Meta token request|response ?headerName ?VALUE??}
- }
-
- set header [string tolower [lindex $args 0]]
- set how [string tolower [lindex $args 1]]
- set lenny [llength $args]
- if {$lenny == 0} {
- return $state($whom)
- } elseif {($lenny > 2) || (($lenny == 2) && ($how ne {value}))} {
- return -code error {usage: ::http::Meta token request|response ?headerName ?VALUE??}
- } else {
- set result {}
- set combined {}
- foreach {key value} $state($whom) {
- if {$key eq $header} {
- lappend result $key $value
- append combined $value {, }
- }
- }
- if {$lenny == 1} {
- return $result
- } else {
- return [string range $combined 0 end-2]
- }
- }
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc http::responseInfo
-# ------------------------------------------------------------------------------
-# Command to return a dictionary of the most useful metadata of a HTTP
-# response.
-#
-# Arguments:
-# token - connection token (name of an array)
-#
-# Return Value: a dict. See man page http(n) for a description of each item.
-# ------------------------------------------------------------------------------
-
-proc http::responseInfo {token} {
- variable $token
- upvar 0 $token state
- set result {}
- foreach {key origin name} {
- stage STATE state
- status STATE status
- responseCode STATE responseCode
- reasonPhrase STATE reasonPhrase
- contentType STATE type
- binary STATE binary
- redirection RESP location
- upgrade STATE upgrade
- error ERROR -
- postError STATE posterror
- method STATE method
- charset STATE charset
- compression STATE coding
- httpRequest STATE -protocol
- httpResponse STATE httpResponse
- url STATE url
- connectionRequest REQ connection
- connectionResponse RESP connection
- connectionActual STATE connection
- transferEncoding STATE transfer
- totalPost STATE querylength
- currentPost STATE queryoffset
- totalSize STATE totalsize
- currentSize STATE currentsize
- proxyUsed STATE proxyUsed
- } {
- if {$origin eq {STATE}} {
- if {[info exists state($name)]} {
- dict set result $key $state($name)
- } else {
- # Should never come here
- dict set result $key {}
- }
- } elseif {$origin eq {REQ}} {
- dict set result $key [requestHeaderValue $token $name]
- } elseif {$origin eq {RESP}} {
- dict set result $key [responseHeaderValue $token $name]
- } elseif {$origin eq {ERROR}} {
- # Don't flood the dict with data. The command ::http::error is
- # available.
- if {[info exists state(error)]} {
- set msg [lindex $state(error) 0]
- } else {
- set msg {}
- }
- dict set result $key $msg
- } else {
- # Should never come here
- dict set result $key {}
- }
- }
- return $result
-}
-proc http::error {token} {
- variable $token
- upvar 0 $token state
- if {[info exists state(error)]} {
- return $state(error)
- }
- return
-}
-proc http::postError {token} {
- variable $token
- upvar 0 $token state
- if {[info exists state(postErrorFull)]} {
- return $state(postErrorFull)
- }
- return
-}
-
-# http::cleanup
-#
-# Garbage collect the state associated with a transaction
-#
-# Arguments
-# token The token returned from http::geturl
-#
-# Side Effects
-# Unsets the state array.
-
-proc http::cleanup {token} {
- variable $token
- upvar 0 $token state
- if {[info commands ${token}--EventCoroutine] ne {}} {
- rename ${token}--EventCoroutine {}
- }
- if {[info commands ${token}--SocketCoroutine] ne {}} {
- rename ${token}--SocketCoroutine {}
- }
- if {[info exists state(after)]} {
- after cancel $state(after)
- unset state(after)
- }
- if {[info exists state(socketcoro)]} {
- Log $token Cancel socket after-idle event (cleanup)
- after cancel $state(socketcoro)
- unset state(socketcoro)
- }
- if {[info exists state]} {
- unset state
- }
- return
-}
-
-# http::Connect
-#
-# This callback is made when an asynchronous connection completes.
-#
-# Arguments
-# token The token returned from http::geturl
-#
-# Side Effects
-# Sets the status of the connection, which unblocks
-# the waiting geturl call
-
-proc http::Connect {token proto phost srvurl} {
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
-
- if {[catch {eof $state(sock)} tmp] || $tmp} {
- set err "due to unexpected EOF"
- } elseif {[set err [fconfigure $state(sock) -error]] ne ""} {
- # set err is done in test
- } else {
- # All OK
- set state(state) connecting
- fileevent $state(sock) writable {}
- ::http::Connected $token $proto $phost $srvurl
- return
- }
-
- # Error cases.
- Log "WARNING - if testing, pay special attention to this\
- case (GJ) which is seldom executed - token $token"
- if {[info exists state(reusing)] && $state(reusing)} {
- # The socket was closed at the server end, and closed at
- # this end by http::CheckEof.
- if {[TestForReplay $token write $err b]} {
- return
- }
-
- # else:
- # This is NOT a persistent socket that has been closed since its
- # last use.
- # If any other requests are in flight or pipelined/queued, they will
- # be discarded.
- }
- Finish $token "connect failed: $err"
- return
-}
-
-# http::Write
-#
-# Write POST query data to the socket
-#
-# Arguments
-# token The token for the connection
-#
-# Side Effects
-# Write the socket and handle callbacks.
-
-proc http::Write {token} {
- variable http
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
- set sock $state(sock)
-
- # Output a block. Tcl will buffer this if the socket blocks
- set done 0
- if {[catch {
- # Catch I/O errors on dead sockets
-
- if {[info exists state(-query)]} {
- # Chop up large query strings so queryprogress callback can give
- # smooth feedback.
- if { $state(queryoffset) + $state(-queryblocksize)
- >= $state(querylength)
- } {
- # This will be the last puts for the request-body.
- if { (![catch {fileevent $sock readable} binding])
- && ($binding eq [list http::CheckEof $sock])
- } {
- # Remove the "fileevent readable" binding of an idle
- # persistent socket to http::CheckEof. We can no longer
- # treat bytes received as junk. The server might still time
- # out and half-close the socket if it has not yet received
- # the first "puts".
- fileevent $sock readable {}
- }
- }
- puts -nonewline $sock \
- [string range $state(-query) $state(queryoffset) \
- [expr {$state(queryoffset) + $state(-queryblocksize) - 1}]]
- incr state(queryoffset) $state(-queryblocksize)
- if {$state(queryoffset) >= $state(querylength)} {
- set state(queryoffset) $state(querylength)
- set done 1
- }
- } else {
- # Copy blocks from the query channel
-
- set outStr [read $state(-querychannel) $state(-queryblocksize)]
- if {[eof $state(-querychannel)]} {
- # This will be the last puts for the request-body.
- if { (![catch {fileevent $sock readable} binding])
- && ($binding eq [list http::CheckEof $sock])
- } {
- # Remove the "fileevent readable" binding of an idle
- # persistent socket to http::CheckEof. We can no longer
- # treat bytes received as junk. The server might still time
- # out and half-close the socket if it has not yet received
- # the first "puts".
- fileevent $sock readable {}
- }
- }
- puts -nonewline $sock $outStr
- incr state(queryoffset) [string length $outStr]
- if {[eof $state(-querychannel)]} {
- set done 1
- }
- }
- } err opts]} {
- # Do not call Finish here, but instead let the read half of the socket
- # process whatever server reply there is to get.
- set state(posterror) $err
- set info [dict get $opts -errorinfo]
- set code [dict get $opts -code]
- set state(postErrorFull) [list $err $info $code]
- set done 1
- }
-
- if {$done} {
- catch {flush $sock}
- fileevent $sock writable {}
- Log ^C$tk end sending request - token $token
- # End of writing (POST method). The request has been sent.
-
- DoneRequest $token
- }
-
- # Callback to the client after we've completely handled everything.
-
- if {[string length $state(-queryprogress)]} {
- namespace eval :: $state(-queryprogress) \
- [list $token $state(querylength) $state(queryoffset)]
- }
- return
-}
-
-# http::Event
-#
-# Handle input on the socket. This command is the core of
-# the coroutine commands ${token}--EventCoroutine that are
-# bound to "fileevent $sock readable" and process input.
-#
-# Arguments
-# sock The socket receiving input.
-# token The token returned from http::geturl
-#
-# Side Effects
-# Read the socket and handle callbacks.
-
-proc http::Event {sock token} {
- variable http
- variable socketMapping
- variable socketRdState
- variable socketWrState
- variable socketRdQueue
- variable socketWrQueue
- variable socketPhQueue
- variable socketClosing
- variable socketPlayCmd
- variable socketCoEvent
- variable socketProxyId
-
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
- while 1 {
- yield
- ##Log Event call - token $token
-
- if {![info exists state]} {
- Log "Event $sock with invalid token '$token' - remote close?"
- if {!([catch {eof $sock} tmp] || $tmp)} {
- if {[set d [read $sock]] ne ""} {
- Log "WARNING: additional data left on closed socket\
- - token $token"
- } else {
- }
- } else {
- }
- Log ^X$tk end of response (token error) - token $token
- CloseSocket $sock
- return
- } else {
- }
- if {$state(state) eq "connecting"} {
- ##Log - connecting - token $token
- if { $state(reusing)
- && $state(-pipeline)
- && ($state(-timeout) > 0)
- && (![info exists state(after)])
- } {
- set state(after) [after $state(-timeout) \
- [list http::reset $token timeout]]
- } else {
- }
-
- if {[catch {gets $sock state(http)} nsl]} {
- Log "WARNING - if testing, pay special attention to this\
- case (GK) which is seldom executed - token $token"
- if {[info exists state(reusing)] && $state(reusing)} {
- # The socket was closed at the server end, and closed at
- # this end by http::CheckEof.
-
- if {[TestForReplay $token read $nsl c]} {
- return
- } else {
- }
- # else:
- # This is NOT a persistent socket that has been closed since
- # its last use.
- # If any other requests are in flight or pipelined/queued,
- # they will be discarded.
- } else {
- # https handshake errors come here, for
- # Tcl 8.7 with http::SecureProxyConnect.
- set msg [registerError $sock]
- registerError $sock {}
- if {$msg eq {}} {
- set msg $nsl
- }
- Log ^X$tk end of response (error) - token $token
- Finish $token $msg
- return
- }
- } elseif {$nsl >= 0} {
- ##Log - connecting 1 - token $token
- set state(state) "header"
- } elseif { ([catch {eof $sock} tmp] || $tmp)
- && [info exists state(reusing)]
- && $state(reusing)
- } {
- # The socket was closed at the server end, and we didn't notice.
- # This is the first read - where the closure is usually first
- # detected.
-
- if {[TestForReplay $token read {} d]} {
- return
- } else {
- }
-
- # else:
- # This is NOT a persistent socket that has been closed since its
- # last use.
- # If any other requests are in flight or pipelined/queued, they
- # will be discarded.
- } else {
- }
- } elseif {$state(state) eq "header"} {
- if {[catch {gets $sock line} nhl]} {
- ##Log header failed - token $token
- Log ^X$tk end of response (error) - token $token
- Finish $token $nhl
- return
- } elseif {$nhl == 0} {
- ##Log header done - token $token
- Log ^E$tk end of response headers - token $token
- # We have now read all headers
- # We ignore HTTP/1.1 100 Continue returns. RFC2616 sec 8.2.3
- if { ($state(http) == "")
- || ([regexp {^\S+\s(\d+)} $state(http) {} x] && $x == 100)
- } {
- set state(state) "connecting"
- continue
- # This was a "return" in the pre-coroutine code.
- } else {
- }
-
- # We have $state(http) so let's split it into its components.
- if {[regexp {^HTTP/(\S+) ([0-9]{3}) (.*)$} $state(http) \
- -> httpResponse responseCode reasonPhrase]
- } {
- set state(httpResponse) $httpResponse
- set state(responseCode) $responseCode
- set state(reasonPhrase) $reasonPhrase
- } else {
- set state(httpResponse) $state(http)
- set state(responseCode) $state(http)
- set state(reasonPhrase) $state(http)
- }
-
- if { ([info exists state(connection)])
- && ([info exists socketMapping($state(socketinfo))])
- && ("keep-alive" in $state(connection))
- && ($state(-keepalive))
- && (!$state(reusing))
- && ($state(-pipeline))
- } {
- # Response headers received for first request on a
- # persistent socket. Now ready for pipelined writes (if
- # any).
- # Previous value is $token. It cannot be "pending".
- set socketWrState($state(socketinfo)) Wready
- http::NextPipelinedWrite $token
- } else {
- }
-
- # Once a "close" has been signaled, the client MUST NOT send any
- # more requests on that connection.
- #
- # If either the client or the server sends the "close" token in
- # the Connection header, that request becomes the last one for
- # the connection.
-
- if { ([info exists state(connection)])
- && ([info exists socketMapping($state(socketinfo))])
- && ("close" in $state(connection))
- && ($state(-keepalive))
- } {
- # The server warns that it will close the socket after this
- # response.
- ##Log WARNING - socket will close after response for $token
- # Prepare data for a call to ReplayIfClose.
- Log $token socket will close after this transaction
- # 1. Cancel socket-assignment coro events that have not yet
- # launched, and add the tokens to the write queue.
- if {[info exists socketCoEvent($state(socketinfo))]} {
- foreach {tok can} $socketCoEvent($state(socketinfo)) {
- lappend socketWrQueue($state(socketinfo)) $tok
- unset -nocomplain ${tok}(socketcoro)
- after cancel $can
- Log $tok Cancel socket after-idle event (Event)
- Log Move $tok from socketCoEvent to socketWrQueue and cancel its after idle coro
- }
- set socketCoEvent($state(socketinfo)) {}
- } else {
- }
-
- if { ($socketRdQueue($state(socketinfo)) ne {})
- || ($socketWrQueue($state(socketinfo)) ne {})
- || ($socketWrState($state(socketinfo)) ni
- [list Wready peNding $token])
- } {
- set InFlightW $socketWrState($state(socketinfo))
- if {$InFlightW in [list Wready peNding $token]} {
- set InFlightW Wready
- } else {
- set msg "token ${InFlightW} is InFlightW"
- ##Log $msg - token $token
- }
- set socketPlayCmd($state(socketinfo)) \
- [list ReplayIfClose $InFlightW \
- $socketRdQueue($state(socketinfo)) \
- $socketWrQueue($state(socketinfo))]
-
- # - All tokens are preserved for re-use by ReplayCore.
- # - Queues are preserved in case of Finish with error,
- # but are not used for anything else because
- # socketClosing(*) is set below.
- # - Cancel the state(after) timeout events.
- foreach tokenVal $socketRdQueue($state(socketinfo)) {
- if {[info exists ${tokenVal}(after)]} {
- after cancel [set ${tokenVal}(after)]
- unset ${tokenVal}(after)
- } else {
- }
- # Tokens in the read queue have no (socketcoro) to
- # cancel.
- }
- } else {
- set socketPlayCmd($state(socketinfo)) \
- {ReplayIfClose Wready {} {}}
- }
-
- # Do not allow further connections on this socket (but
- # geturl can add new requests to the replay).
- set socketClosing($state(socketinfo)) 1
- } else {
- }
-
- set state(state) body
-
- # According to
- # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection
- # any comma-separated "Connection:" list implies keep-alive, but I
- # don't see this in the RFC so we'll play safe and
- # scan any list for "close".
- # Done here to support combining duplicate header field's values.
- if { [info exists state(connection)]
- && ("close" ni $state(connection))
- && ("keep-alive" ni $state(connection))
- } {
- lappend state(connection) "keep-alive"
- } else {
- }
-
- # If doing a HEAD, then we won't get any body
- if {$state(-validate)} {
- Log ^F$tk end of response for HEAD request - token $token
- set state(state) complete
- Eot $token
- return
- } elseif {
- ($state(method) eq {CONNECT})
- && [string is integer -strict $state(responseCode)]
- && ($state(responseCode) >= 200)
- && ($state(responseCode) < 300)
- } {
- # A successful CONNECT response has no body.
- # (An unsuccessful CONNECT has headers and body.)
- # The code below is abstracted from Eot/Finish, but
- # keeps the socket open.
- catch {fileevent $state(sock) readable {}}
- catch {fileevent $state(sock) writable {}}
- set state(state) complete
- set state(status) ok
- if {[info commands ${token}--EventCoroutine] ne {}} {
- rename ${token}--EventCoroutine {}
- }
- if {[info commands ${token}--SocketCoroutine] ne {}} {
- rename ${token}--SocketCoroutine {}
- }
- if {[info exists state(socketcoro)]} {
- Log $token Cancel socket after-idle event (Finish)
- after cancel $state(socketcoro)
- unset state(socketcoro)
- }
- if {[info exists state(after)]} {
- after cancel $state(after)
- unset state(after)
- }
- if { [info exists state(-command)]
- && (![info exists state(done-command-cb)])
- } {
- set state(done-command-cb) yes
- if {[catch {namespace eval :: $state(-command) $token} err]} {
- set state(error) [list $err $errorInfo $errorCode]
- set state(status) error
- }
- }
- return
- } else {
- }
-
- # - For non-chunked transfer we may have no body - in this case
- # we may get no further file event if the connection doesn't
- # close and no more data is sent. We can tell and must finish
- # up now - not later - the alternative would be to wait until
- # the server times out.
- # - In this case, the server has NOT told the client it will
- # close the connection, AND it has NOT indicated the resource
- # length EITHER by setting the Content-Length (totalsize) OR
- # by using chunked Transfer-Encoding.
- # - Do not worry here about the case (Connection: close) because
- # the server should close the connection.
- # - IF (NOT Connection: close) AND (NOT chunked encoding) AND
- # (totalsize == 0).
-
- if { (!( [info exists state(connection)]
- && ("close" in $state(connection))
- )
- )
- && ($state(transfer) eq {})
- && ($state(totalsize) == 0)
- } {
- set msg {body size is 0 and no events likely - complete}
- Log "$msg - token $token"
- set msg {(length unknown, set to 0)}
- Log ^F$tk end of response body {*}$msg - token $token
- set state(state) complete
- Eot $token
- return
- } else {
- }
-
- # We have to use binary translation to count bytes properly.
- lassign [fconfigure $sock -translation] trRead trWrite
- fconfigure $sock -translation [list binary $trWrite]
-
- if {
- $state(-binary) || [IsBinaryContentType $state(type)]
- } {
- # Turn off conversions for non-text data.
- set state(binary) 1
- } else {
- }
- if {[info exists state(-channel)]} {
- if {$state(binary) || [llength [ContentEncoding $token]]} {
- fconfigure $state(-channel) -translation binary
- } else {
- }
- if {![info exists state(-handler)]} {
- # Initiate a sequence of background fcopies.
- fileevent $sock readable {}
- rename ${token}--EventCoroutine {}
- CopyStart $sock $token
- return
- } else {
- }
- } else {
- }
- } elseif {$nhl > 0} {
- # Process header lines.
- ##Log header - token $token - $line
- if {[regexp -nocase {^([^:]+):(.+)$} $line x key value]} {
- set key [string tolower $key]
- switch -- $key {
- content-type {
- set state(type) [string trim [string tolower $value]]
- # Grab the optional charset information.
- if {[regexp -nocase \
- {charset\s*=\s*\"((?:[^""]|\\\")*)\"} \
- $state(type) -> cs]} {
- set state(charset) [string map {{\"} \"} $cs]
- } else {
- regexp -nocase {charset\s*=\s*(\S+?);?} \
- $state(type) -> state(charset)
- }
- }
- content-length {
- set state(totalsize) [string trim $value]
- }
- content-encoding {
- set state(coding) [string trim $value]
- }
- transfer-encoding {
- set state(transfer) \
- [string trim [string tolower $value]]
- }
- proxy-connection -
- connection {
- # RFC 7230 Section 6.1 states that a comma-separated
- # list is an acceptable value.
- if {![info exists state(connectionRespFlag)]} {
- # This is the first "Connection" response header.
- # Scrub the earlier value set by iniitialisation.
- set state(connectionRespFlag) {}
- set state(connection) {}
- }
- foreach el [SplitCommaSeparatedFieldValue $value] {
- lappend state(connection) [string tolower $el]
- }
- }
- upgrade {
- set state(upgrade) [string trim $value]
- }
- set-cookie {
- if {$http(-cookiejar) ne ""} {
- ParseCookie $token [string trim $value]
- } else {
- }
- }
- }
- lappend state(meta) $key [string trim $value]
- } else {
- }
- } else {
- }
- } else {
- # Now reading body
- ##Log body - token $token
- if {[catch {
- if {[info exists state(-handler)]} {
- set n [namespace eval :: $state(-handler) [list $sock $token]]
- ##Log handler $n - token $token
- # N.B. the protocol has been set to 1.0 because the -handler
- # logic is not expected to handle chunked encoding.
- # FIXME Allow -handler with 1.1 on dechunked stacked chan.
- if {$state(totalsize) == 0} {
- # We know the transfer is complete only when the server
- # closes the connection - i.e. eof is not an error.
- set state(state) complete
- } else {
- }
- if {![string is integer -strict $n]} {
- if 1 {
- # Do not tolerate bad -handler - fail with error
- # status.
- set msg {the -handler command for http::geturl must\
- return an integer (the number of bytes\
- read)}
- Log ^X$tk end of response (handler error) -\
- token $token
- Eot $token $msg
- } else {
- # Tolerate the bad -handler, and continue. The
- # penalty:
- # (a) Because the handler returns nonsense, we know
- # the transfer is complete only when the server
- # closes the connection - i.e. eof is not an
- # error.
- # (b) http::size will not be accurate.
- # (c) The transaction is already downgraded to 1.0
- # to avoid chunked transfer encoding. It MUST
- # also be forced to "Connection: close" or the
- # HTTP/1.0 equivalent; or it MUST fail (as
- # above) if the server sends
- # "Connection: keep-alive" or the HTTP/1.0
- # equivalent.
- set n 0
- set state(state) complete
- }
- } else {
- }
- } elseif {[info exists state(transfer_final)]} {
- # This code forgives EOF in place of the final CRLF.
- set line [GetTextLine $sock]
- set n [string length $line]
- set state(state) complete
- if {$n > 0} {
- # - HTTP trailers (late response headers) are permitted
- # by Chunked Transfer-Encoding, and can be safely
- # ignored.
- # - Do not count these bytes in the total received for
- # the response body.
- Log "trailer of $n bytes after final chunk -\
- token $token"
- append state(transfer_final) $line
- set n 0
- } else {
- Log ^F$tk end of response body (chunked) - token $token
- Log "final chunk part - token $token"
- Eot $token
- }
- } elseif { [info exists state(transfer)]
- && ($state(transfer) eq "chunked")
- } {
- ##Log chunked - token $token
- set size 0
- set hexLenChunk [GetTextLine $sock]
- #set ntl [string length $hexLenChunk]
- if {[string trim $hexLenChunk] ne ""} {
- scan $hexLenChunk %x size
- if {$size != 0} {
- ##Log chunk-measure $size - token $token
- set chunk [BlockingRead $sock $size]
- set n [string length $chunk]
- if {$n >= 0} {
- append state(body) $chunk
- incr state(log_size) [string length $chunk]
- ##Log chunk $n cumul $state(log_size) -\
- token $token
- } else {
- }
- if {$size != [string length $chunk]} {
- Log "WARNING: mis-sized chunk:\
- was [string length $chunk], should be\
- $size - token $token"
- set n 0
- set state(connection) close
- Log ^X$tk end of response (chunk error) \
- - token $token
- set msg {error in chunked encoding - fetch\
- terminated}
- Eot $token $msg
- } else {
- }
- # CRLF that follows chunk.
- # If eof, this is handled at the end of this proc.
- GetTextLine $sock
- } else {
- set n 0
- set state(transfer_final) {}
- }
- } else {
- # Line expected to hold chunk length is empty, or eof.
- ##Log bad-chunk-measure - token $token
- set n 0
- set state(connection) close
- Log ^X$tk end of response (chunk error) - token $token
- Eot $token {error in chunked encoding -\
- fetch terminated}
- }
- } else {
- ##Log unchunked - token $token
- if {$state(totalsize) == 0} {
- # We know the transfer is complete only when the server
- # closes the connection.
- set state(state) complete
- set reqSize $state(-blocksize)
- } else {
- # Ask for the whole of the unserved response-body.
- # This works around a problem with a tls::socket - for
- # https in keep-alive mode, and a request for
- # $state(-blocksize) bytes, the last part of the
- # resource does not get read until the server times out.
- set reqSize [expr { $state(totalsize)
- - $state(currentsize)}]
-
- # The workaround fails if reqSize is
- # capped at $state(-blocksize).
- # set reqSize [expr {min($reqSize, $state(-blocksize))}]
- }
- set c $state(currentsize)
- set t $state(totalsize)
- ##Log non-chunk currentsize $c of totalsize $t -\
- token $token
- set block [read $sock $reqSize]
- set n [string length $block]
- if {$n >= 0} {
- append state(body) $block
- ##Log non-chunk [string length $state(body)] -\
- token $token
- } else {
- }
- }
- # This calculation uses n from the -handler, chunked, or
- # unchunked case as appropriate.
- if {[info exists state]} {
- if {$n >= 0} {
- incr state(currentsize) $n
- set c $state(currentsize)
- set t $state(totalsize)
- ##Log another $n currentsize $c totalsize $t -\
- token $token
- } else {
- }
- # If Content-Length - check for end of data.
- if {
- ($state(totalsize) > 0)
- && ($state(currentsize) >= $state(totalsize))
- } {
- Log ^F$tk end of response body (unchunked) -\
- token $token
- set state(state) complete
- Eot $token
- } else {
- }
- } else {
- }
- } err]} {
- Log ^X$tk end of response (error ${err}) - token $token
- Finish $token $err
- return
- } else {
- if {[info exists state(-progress)]} {
- namespace eval :: $state(-progress) \
- [list $token $state(totalsize) $state(currentsize)]
- } else {
- }
- }
- }
-
- # catch as an Eot above may have closed the socket already
- # $state(state) may be connecting, header, body, or complete
- if {(![catch {eof $sock} eof]) && $eof} {
- # [eof sock] succeeded and the result was 1
- ##Log eof - token $token
- if {[info exists $token]} {
- set state(connection) close
- if {$state(state) eq "complete"} {
- # This includes all cases in which the transaction
- # can be completed by eof.
- # The value "complete" is set only in http::Event, and it is
- # used only in the test above.
- Log ^F$tk end of response body (unchunked, eof) -\
- token $token
- Eot $token
- } else {
- # Premature eof.
- Log ^X$tk end of response (unexpected eof) - token $token
- Eot $token eof
- }
- } else {
- # open connection closed on a token that has been cleaned up.
- Log ^X$tk end of response (token error) - token $token
- CloseSocket $sock
- }
- } else {
- # EITHER [eof sock] failed - presumed done by Eot
- # OR [eof sock] succeeded and the result was 0
- }
- }
- return
-}
-
-# http::TestForReplay
-#
-# Command called if eof is discovered when a socket is first used for a
-# new transaction. Typically this occurs if a persistent socket is used
-# after a period of idleness and the server has half-closed the socket.
-#
-# token - the connection token returned by http::geturl
-# doing - "read" or "write"
-# err - error message, if any
-# caller - code to identify the caller - used only in logging
-#
-# Return Value: boolean, true iff the command calls http::ReplayIfDead.
-
-proc http::TestForReplay {token doing err caller} {
- variable http
- variable $token
- upvar 0 $token state
- set tk [namespace tail $token]
- if {$doing eq "read"} {
- set code Q
- set action response
- set ing reading
- } else {
- set code P
- set action request
- set ing writing
- }
-
- if {$err eq {}} {
- set err "detect eof when $ing (server timed out?)"
- }
-
- if {$state(method) eq "POST" && !$http(-repost)} {
- # No Replay.
- # The present transaction will end when Finish is called.
- # That call to Finish will abort any other transactions
- # currently in the write queue.
- # For calls from http::Event this occurs when execution
- # reaches the code block at the end of that proc.
- set msg {no retry for POST with http::config -repost 0}
- Log reusing socket failed "($caller)" - $msg - token $token
- Log error - $err - token $token
- Log ^X$tk end of $action (error) - token $token
- return 0
- } else {
- # Replay.
- set msg {try a new socket}
- Log reusing socket failed "($caller)" - $msg - token $token
- Log error - $err - token $token
- Log ^$code$tk Any unfinished (incl this one) failed - token $token
- ReplayIfDead $token $doing
- return 1
- }
-}
-
-# http::IsBinaryContentType --
-#
-# Determine if the content-type means that we should definitely transfer
-# the data as binary. [Bug 838e99a76d]
-#
-# Arguments
-# type The content-type of the data.
-#
-# Results:
-# Boolean, true if we definitely should be binary.
-
-proc http::IsBinaryContentType {type} {
- lassign [split [string tolower $type] "/;"] major minor
- if {$major eq "text"} {
- return false
- }
- # There's a bunch of XML-as-application-format things about. See RFC 3023
- # and so on.
- if {$major eq "application"} {
- set minor [string trimright $minor]
- if {$minor in {"json" "xml" "xml-external-parsed-entity" "xml-dtd"}} {
- return false
- }
- }
- # Not just application/foobar+xml but also image/svg+xml, so let us not
- # restrict things for now...
- if {[string match "*+xml" $minor]} {
- return false
- }
- return true
-}
-
-proc http::ParseCookie {token value} {
- variable http
- variable CookieRE
- variable $token
- upvar 0 $token state
-
- if {![regexp $CookieRE $value -> cookiename cookieval opts]} {
- # Bad cookie! No biscuit!
- return
- }
-
- # Convert the options into a list before feeding into the cookie store;
- # ugly, but quite easy.
- set realopts {hostonly 1 path / secure 0 httponly 0}
- dict set realopts origin $state(host)
- dict set realopts domain $state(host)
- foreach option [split [regsub -all {;\s+} $opts \u0000] \u0000] {
- regexp {^(.*?)(?:=(.*))?$} $option -> optname optval
- switch -exact -- [string tolower $optname] {
- expires {
- if {[catch {
- #Sun, 06 Nov 1994 08:49:37 GMT
- dict set realopts expires \
- [clock scan $optval -format "%a, %d %b %Y %T %Z"]
- }] && [catch {
- # Google does this one
- #Mon, 01-Jan-1990 00:00:00 GMT
- dict set realopts expires \
- [clock scan $optval -format "%a, %d-%b-%Y %T %Z"]
- }] && [catch {
- # This is in the RFC, but it is also in the original
- # Netscape cookie spec, now online at:
- #
- #Sunday, 06-Nov-94 08:49:37 GMT
- dict set realopts expires \
- [clock scan $optval -format "%A, %d-%b-%y %T %Z"]
- }]} {catch {
- #Sun Nov 6 08:49:37 1994
- dict set realopts expires \
- [clock scan $optval -gmt 1 -format "%a %b %d %T %Y"]
- }}
- }
- max-age {
- # Normalize
- if {[string is integer -strict $optval]} {
- dict set realopts expires [expr {[clock seconds] + $optval}]
- }
- }
- domain {
- # From the domain-matches definition [RFC 2109, section 2]:
- # Host A's name domain-matches host B's if [...]
- # A is a FQDN string and has the form NB, where N is a
- # non-empty name string, B has the form .B', and B' is a
- # FQDN string. (So, x.y.com domain-matches .y.com but
- # not y.com.)
- if {$optval ne "" && ![string match *. $optval]} {
- dict set realopts domain [string trimleft $optval "."]
- dict set realopts hostonly [expr {
- ! [string match .* $optval]
- }]
- }
- }
- path {
- if {[string match /* $optval]} {
- dict set realopts path $optval
- }
- }
- secure - httponly {
- dict set realopts [string tolower $optname] 1
- }
- }
- }
- dict set realopts key $cookiename
- dict set realopts value $cookieval
- {*}$http(-cookiejar) storeCookie $realopts
-}
-
-# http::GetTextLine --
-#
-# Get one line with the stream in crlf mode.
-# Used if Transfer-Encoding is chunked, to read the line that
-# reports the size of the following chunk.
-# Empty line is not distinguished from eof. The caller must
-# be able to handle this.
-#
-# Arguments
-# sock The socket receiving input.
-#
-# Results:
-# The line of text, without trailing newline
-
-proc http::GetTextLine {sock} {
- set tr [fconfigure $sock -translation]
- lassign $tr trRead trWrite
- fconfigure $sock -translation [list crlf $trWrite]
- set r [BlockingGets $sock]
- fconfigure $sock -translation $tr
- return $r
-}
-
-# http::BlockingRead
-#
-# Replacement for a blocking read.
-# The caller must be a coroutine.
-# Used when we expect to read a chunked-encoding
-# chunk of known size.
-
-proc http::BlockingRead {sock size} {
- if {$size < 1} {
- return
- }
- set result {}
- while 1 {
- set need [expr {$size - [string length $result]}]
- set block [read $sock $need]
- set eof [expr {[catch {eof $sock} tmp] || $tmp}]
- append result $block
- if {[string length $result] >= $size || $eof} {
- return $result
- } else {
- yield
- }
- }
-}
-
-# http::BlockingGets
-#
-# Replacement for a blocking gets.
-# The caller must be a coroutine.
-# Empty line is not distinguished from eof. The caller must
-# be able to handle this.
-
-proc http::BlockingGets {sock} {
- while 1 {
- set count [gets $sock line]
- set eof [expr {[catch {eof $sock} tmp] || $tmp}]
- if {$count >= 0 || $eof} {
- return $line
- } else {
- yield
- }
- }
-}
-
-# http::CopyStart
-#
-# Error handling wrapper around fcopy
-#
-# Arguments
-# sock The socket to copy from
-# token The token returned from http::geturl
-#
-# Side Effects
-# This closes the connection upon error
-
-proc http::CopyStart {sock token {initial 1}} {
- upvar 0 $token state
- if {[info exists state(transfer)] && $state(transfer) eq "chunked"} {
- foreach coding [ContentEncoding $token] {
- if {$coding eq {deflateX}} {
- # Use the standards-compliant choice.
- set coding2 decompress
- } else {
- set coding2 $coding
- }
- lappend state(zlib) [zlib stream $coding2]
- }
- MakeTransformationChunked $sock [namespace code [list CopyChunk $token]]
- } else {
- if {$initial} {
- foreach coding [ContentEncoding $token] {
- if {$coding eq {deflateX}} {
- # Use the standards-compliant choice.
- set coding2 decompress
- } else {
- set coding2 $coding
- }
- zlib push $coding2 $sock
- }
- }
- if {[catch {
- # FIXME Keep-Alive on https tls::socket with unchunked transfer
- # hangs until the server times out. A workaround is possible, as for
- # the case without -channel, but it does not use the neat "fcopy"
- # solution.
- fcopy $sock $state(-channel) -size $state(-blocksize) -command \
- [list http::CopyDone $token]
- } err]} {
- Finish $token $err
- }
- }
- return
-}
-
-proc http::CopyChunk {token chunk} {
- upvar 0 $token state
- if {[set count [string length $chunk]]} {
- incr state(currentsize) $count
- if {[info exists state(zlib)]} {
- foreach stream $state(zlib) {
- set chunk [$stream add $chunk]
- }
- }
- puts -nonewline $state(-channel) $chunk
- if {[info exists state(-progress)]} {
- namespace eval :: [linsert $state(-progress) end \
- $token $state(totalsize) $state(currentsize)]
- }
- } else {
- Log "CopyChunk Finish - token $token"
- if {[info exists state(zlib)]} {
- set excess ""
- foreach stream $state(zlib) {
- catch {
- $stream put -finalize $excess
- set excess ""
- set overflood ""
- while {[set overflood [$stream get]] ne ""} { append excess $overflood }
- }
- }
- puts -nonewline $state(-channel) $excess
- foreach stream $state(zlib) { $stream close }
- unset state(zlib)
- }
- Eot $token ;# FIX ME: pipelining.
- }
- return
-}
-
-# http::CopyDone
-#
-# fcopy completion callback
-#
-# Arguments
-# token The token returned from http::geturl
-# count The amount transferred
-#
-# Side Effects
-# Invokes callbacks
-
-proc http::CopyDone {token count {error {}}} {
- variable $token
- upvar 0 $token state
- set sock $state(sock)
- incr state(currentsize) $count
- if {[info exists state(-progress)]} {
- namespace eval :: $state(-progress) \
- [list $token $state(totalsize) $state(currentsize)]
- }
- # At this point the token may have been reset.
- if {[string length $error]} {
- Finish $token $error
- } elseif {[catch {eof $sock} iseof] || $iseof} {
- Eot $token
- } else {
- CopyStart $sock $token 0
- }
- return
-}
-
-# http::Eot
-#
-# Called when either:
-# a. An eof condition is detected on the socket.
-# b. The client decides that the response is complete.
-# c. The client detects an inconsistency and aborts the transaction.
-#
-# Does:
-# 1. Set state(status)
-# 2. Reverse any Content-Encoding
-# 3. Convert charset encoding and line ends if necessary
-# 4. Call http::Finish
-#
-# Arguments
-# token The token returned from http::geturl
-# force (previously) optional, has no effect
-# reason - "eof" means premature EOF (not EOF as the natural end of
-# the response)
-# - "" means completion of response, with or without EOF
-# - anything else describes an error condition other than
-# premature EOF.
-#
-# Side Effects
-# Clean up the socket
-
-proc http::Eot {token {reason {}}} {
- variable $token
- upvar 0 $token state
- if {$reason eq "eof"} {
- # Premature eof.
- set state(status) eof
- set reason {}
- } elseif {$reason ne ""} {
- # Abort the transaction.
- set state(status) $reason
- } else {
- # The response is complete.
- set state(status) ok
- }
-
- if {[string length $state(body)] > 0} {
- if {[catch {
- foreach coding [ContentEncoding $token] {
- if {$coding eq {deflateX}} {
- # First try the standards-compliant choice.
- set coding2 decompress
- if {[catch {zlib $coding2 $state(body)} result]} {
- # If that fails, try the MS non-compliant choice.
- set coding2 inflate
- set state(body) [zlib $coding2 $state(body)]
- } else {
- # error {failed at standards-compliant deflate}
- set state(body) $result
- }
- } else {
- set state(body) [zlib $coding $state(body)]
- }
- }
- } err]} {
- Log "error doing decompression for token $token: $err"
- Finish $token $err
- return
- }
-
- if {!$state(binary)} {
- # If we are getting text, set the incoming channel's encoding
- # correctly. iso8859-1 is the RFC default, but this could be any
- # IANA charset. However, we only know how to convert what we have
- # encodings for.
-
- set enc [CharsetToEncoding $state(charset)]
- if {$enc ne "binary"} {
- if {[package vsatisfies [package provide Tcl] 9.0-]} {
- set state(body) [encoding convertfrom -profile tcl8 $enc $state(body)]
- } else {
- set state(body) [encoding convertfrom $enc $state(body)]
- }
- }
-
- # Translate text line endings.
- set state(body) [string map {\r\n \n \r \n} $state(body)]
- }
- if {[info exists state(-guesstype)] && $state(-guesstype)} {
- GuessType $token
- }
- }
- Finish $token $reason
- return
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc http::GuessType
-# ------------------------------------------------------------------------------
-# Command to attempt limited analysis of a resource with undetermined
-# Content-Type, i.e. "application/octet-stream". This value can be set for two
-# reasons:
-# (a) by the server, in a Content-Type header
-# (b) by http::geturl, as the default value if the server does not supply a
-# Content-Type header.
-#
-# This command converts a resource if:
-# (1) it has type application/octet-stream
-# (2) it begins with an XML declaration "?"
-# (3) one tag is named "encoding" and has a recognised value; or no "encoding"
-# tag exists (defaulting to utf-8)
-#
-# RFC 9110 Sec. 8.3 states:
-# "If a Content-Type header field is not present, the recipient MAY either
-# assume a media type of "application/octet-stream" ([RFC2046], Section 4.5.1)
-# or examine the data to determine its type."
-#
-# The RFC goes on to describe the pitfalls of "MIME sniffing", including
-# possible security risks.
-#
-# Arguments:
-# token - connection token
-#
-# Return Value: (boolean) true iff a change has been made
-# ------------------------------------------------------------------------------
-
-proc http::GuessType {token} {
- variable $token
- upvar 0 $token state
-
- if {$state(type) ne {application/octet-stream}} {
- return 0
- }
-
- set body $state(body)
- # e.g. { ...}
-
- if {![regexp -nocase -- {^<[?]xml[[:space:]][^>?]*[?]>} $body match]} {
- return 0
- }
- # e.g. {}
-
- set contents [regsub -- {[[:space:]]+} $match { }]
- set contents [string range [string tolower $contents] 6 end-2]
- # e.g. {version="1.0" encoding="utf-8"}
- # without excess whitespace or upper-case letters
-
- if {![regexp -- {^([^=" ]+="[^"]+" )+$} "$contents "]} {
- return 0
- }
- # The application/xml default encoding:
- set res utf-8
-
- set tagList [regexp -all -inline -- {[^=" ]+="[^"]+"} $contents]
- foreach tag $tagList {
- regexp -- {([^=" ]+)="([^"]+)"} $tag -> name value
- if {$name eq {encoding}} {
- set res $value
- }
- }
- set enc [CharsetToEncoding $res]
- if {$enc eq "binary"} {
- return 0
- }
- if {[package vsatisfies [package provide Tcl] 9.0-]} {
- set state(body) [encoding convertfrom -profile tcl8 $enc $state(body)]
- } else {
- set state(body) [encoding convertfrom $enc $state(body)]
- }
- set state(body) [string map {\r\n \n \r \n} $state(body)]
- set state(type) application/xml
- set state(binary) 0
- set state(charset) $res
- return 1
-}
-
-
-# http::wait --
-#
-# See documentation for details.
-#
-# Arguments:
-# token Connection token.
-#
-# Results:
-# The status after the wait.
-
-proc http::wait {token} {
- variable $token
- upvar 0 $token state
-
- if {![info exists state(status)] || $state(status) eq ""} {
- # We must wait on the original variable name, not the upvar alias
- vwait ${token}(status)
- }
-
- return [status $token]
-}
-
-# http::formatQuery --
-#
-# See documentation for details. Call http::formatQuery with an even
-# number of arguments, where the first is a name, the second is a value,
-# the third is another name, and so on.
-#
-# Arguments:
-# args A list of name-value pairs.
-#
-# Results:
-# TODO
-
-proc http::formatQuery {args} {
- if {[llength $args] % 2} {
- return \
- -code error \
- -errorcode [list HTTP BADARGCNT $args] \
- {Incorrect number of arguments, must be an even number.}
- }
- set result ""
- set sep ""
- foreach i $args {
- append result $sep [quoteString $i]
- if {$sep eq "="} {
- set sep &
- } else {
- set sep =
- }
- }
- return $result
-}
-
-# http::quoteString --
-#
-# Do x-www-urlencoded character mapping
-#
-# Arguments:
-# string The string the needs to be encoded
-#
-# Results:
-# The encoded string
-
-proc http::quoteString {string} {
- variable http
- variable formMap
-
- # The spec says: "non-alphanumeric characters are replaced by '%HH'". Use
- # a pre-computed map and [string map] to do the conversion (much faster
- # than [regsub]/[subst]). [Bug 1020491]
-
- if {[package vsatisfies [package provide Tcl] 9.0-]} {
- set string [encoding convertto -profile tcl8 $http(-urlencoding) $string]
- } else {
- set string [encoding convertto $http(-urlencoding) $string]
- }
- return [string map $formMap $string]
-}
-
-# http::ProxyRequired --
-# Default proxy filter.
-#
-# Arguments:
-# host The destination host
-#
-# Results:
-# The current proxy settings
-
-proc http::ProxyRequired {host} {
- variable http
- if {(![info exists http(-proxyhost)]) || ($http(-proxyhost) eq {})} {
- return
- }
- if {![info exists http(-proxyport)] || ($http(-proxyport) eq {})} {
- set port 8080
- } else {
- set port $http(-proxyport)
- }
-
- # Simple test (cf. autoproxy) for hosts that must be accessed directly,
- # not through the proxy server.
- foreach domain $http(-proxynot) {
- if {[string match -nocase $domain $host]} {
- return {}
- }
- }
- return [list $http(-proxyhost) $port]
-}
-
-# http::CharsetToEncoding --
-#
-# Tries to map a given IANA charset to a tcl encoding. If no encoding
-# can be found, returns binary.
-#
-
-proc http::CharsetToEncoding {charset} {
- variable encodings
-
- set charset [string tolower $charset]
- if {[regexp {iso-?8859-([0-9]+)} $charset -> num]} {
- set encoding "iso8859-$num"
- } elseif {[regexp {iso-?2022-(jp|kr)} $charset -> ext]} {
- set encoding "iso2022-$ext"
- } elseif {[regexp {shift[-_]?jis} $charset]} {
- set encoding "shiftjis"
- } elseif {[regexp {(?:windows|cp)-?([0-9]+)} $charset -> num]} {
- set encoding "cp$num"
- } elseif {$charset eq "us-ascii"} {
- set encoding "ascii"
- } elseif {[regexp {(?:iso-?)?lat(?:in)?-?([0-9]+)} $charset -> num]} {
- switch -- $num {
- 5 {set encoding "iso8859-9"}
- 1 - 2 - 3 {
- set encoding "iso8859-$num"
- }
- default {
- set encoding "binary"
- }
- }
- } else {
- # other charset, like euc-xx, utf-8,... may directly map to encoding
- set encoding $charset
- }
- set idx [lsearch -exact $encodings $encoding]
- if {$idx >= 0} {
- return $encoding
- } else {
- return "binary"
- }
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc http::ContentEncoding
-# ------------------------------------------------------------------------------
-# Return the list of content-encoding transformations we need to do in order.
-#
- # --------------------------------------------------------------------------
- # Options for Accept-Encoding, Content-Encoding: the switch command
- # --------------------------------------------------------------------------
- # The symbol deflateX allows http to attempt both versions of "deflate",
- # unless there is a -channel - for a -channel, only "decompress" is tried.
- # Alternative/extra lines for switch:
- # The standards-compliant version of "deflate" can be chosen with:
- # deflate { lappend r decompress }
- # The Microsoft non-compliant version of "deflate" can be chosen with:
- # deflate { lappend r inflate }
- # The previously used implementation of "compress", which appears to be
- # incorrect and is rarely used by web servers, can be chosen with:
- # compress - x-compress { lappend r decompress }
- # --------------------------------------------------------------------------
-#
-# Arguments:
-# token - Connection token.
-#
-# Return Value: list
-# ------------------------------------------------------------------------------
-
-proc http::ContentEncoding {token} {
- upvar 0 $token state
- set r {}
- if {[info exists state(coding)]} {
- foreach coding [split $state(coding) ,] {
- switch -exact -- $coding {
- deflate { lappend r deflateX }
- gzip - x-gzip { lappend r gunzip }
- identity {}
- br {
- return -code error\
- "content-encoding \"br\" not implemented"
- }
- default {
- Log "unknown content-encoding \"$coding\" ignored"
- }
- }
- }
- }
- return $r
-}
-
-proc http::ReceiveChunked {chan command} {
- set data ""
- set size -1
- yield
- while {1} {
- chan configure $chan -translation {crlf binary}
- while {[gets $chan line] < 1} { yield }
- chan configure $chan -translation {binary binary}
- if {[scan $line %x size] != 1} {
- return -code error "invalid size: \"$line\""
- }
- set chunk ""
- while {$size && ![chan eof $chan]} {
- set part [chan read $chan $size]
- incr size -[string length $part]
- append chunk $part
- }
- if {[catch {
- uplevel #0 [linsert $command end $chunk]
- }]} {
- http::Log "Error in callback: $::errorInfo"
- }
- if {[string length $chunk] == 0} {
- # channel might have been closed in the callback
- catch {chan event $chan readable {}}
- return
- }
- }
-}
-
-# http::SplitCommaSeparatedFieldValue --
-# Return the individual values of a comma-separated field value.
-#
-# Arguments:
-# fieldValue Comma-separated header field value.
-#
-# Results:
-# List of values.
-proc http::SplitCommaSeparatedFieldValue {fieldValue} {
- set r {}
- foreach el [split $fieldValue ,] {
- lappend r [string trim $el]
- }
- return $r
-}
-
-
-# http::GetFieldValue --
-# Return the value of a header field.
-#
-# Arguments:
-# headers Headers key-value list
-# fieldName Name of header field whose value to return.
-#
-# Results:
-# The value of the fieldName header field
-#
-# Field names are matched case-insensitively (RFC 7230 Section 3.2).
-#
-# If the field is present multiple times, it is assumed that the field is
-# defined as a comma-separated list and the values are combined (by separating
-# them with commas, see RFC 7230 Section 3.2.2) and returned at once.
-proc http::GetFieldValue {headers fieldName} {
- set r {}
- foreach {field value} $headers {
- if {[string equal -nocase $fieldName $field]} {
- if {$r eq {}} {
- set r $value
- } else {
- append r ", $value"
- }
- }
- }
- return $r
-}
-
-proc http::MakeTransformationChunked {chan command} {
- coroutine [namespace current]::dechunk$chan ::http::ReceiveChunked $chan $command
- chan event $chan readable [namespace current]::dechunk$chan
- return
-}
-
-interp alias {} http::data {} http::responseBody
-interp alias {} http::code {} http::responseLine
-interp alias {} http::mapReply {} http::quoteString
-interp alias {} http::meta {} http::responseHeaders
-interp alias {} http::metaValue {} http::responseHeaderValue
-interp alias {} http::ncode {} http::responseCode
-
-
-# ------------------------------------------------------------------------------
-# Proc http::socketForTls
-# ------------------------------------------------------------------------------
-# Command to use in place of ::socket as the value of ::tls::socketCmd.
-# This command does the same as http::socket, and also handles https connections
-# through a proxy server.
-#
-# Notes.
-# - The proxy server works differently for https and http. This implementation
-# is for https. The proxy for http is implemented in http::CreateToken (in
-# code that was previously part of http::geturl).
-# - This code implicitly uses the tls options set for https in a call to
-# http::register, and does not need to call commands tls::*. This simple
-# implementation is possible because tls uses a callback to ::socket that can
-# be redirected by changing the value of ::tls::socketCmd.
-#
-# Arguments:
-# args - as for ::socket
-#
-# Return Value: a socket identifier
-# ------------------------------------------------------------------------------
-
-proc http::socketForTls {args} {
- variable http
- set host [lindex $args end-1]
- set port [lindex $args end]
- if { ($http(-proxyfilter) ne {})
- && (![catch {$http(-proxyfilter) $host} proxy])
- } {
- set phost [lindex $proxy 0]
- set pport [lindex $proxy 1]
- } else {
- set phost {}
- set pport {}
- }
- if {$phost eq ""} {
- set sock [::http::socket {*}$args]
- } else {
- set sock [::http::SecureProxyConnect {*}$args $phost $pport]
- }
- return $sock
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc http::SecureProxyConnect
-# ------------------------------------------------------------------------------
-# Command to open a socket through a proxy server to a remote server for use by
-# tls. The caller must perform the tls handshake.
-#
-# Notes
-# - Based on patch supplied by Melissa Chawla in ticket 1173760, and
-# Proxy-Authorization header cf. autoproxy by Pat Thoyts.
-# - Rewritten as a call to http::geturl, because response headers and body are
-# needed if the CONNECT request fails. CONNECT is implemented for this case
-# only, by state(bypass).
-# - FUTURE WORK: give http::geturl a -connect option for a general CONNECT.
-# - The request header Proxy-Connection is discouraged in RFC 7230 (June 2014),
-# RFC 9112 (June 2022).
-#
-# Arguments:
-# args - as for ::socket, ending in host, port; with proxy host, proxy
-# port appended.
-#
-# Return Value: a socket identifier
-# ------------------------------------------------------------------------------
-
-proc http::SecureProxyConnect {args} {
- variable http
- variable ConnectVar
- variable ConnectCounter
- variable failedProxyValues
- set varName ::http::ConnectVar([incr ConnectCounter])
-
- # Extract (non-proxy) target from args.
- set host [lindex $args end-3]
- set port [lindex $args end-2]
- set args [lreplace $args end-3 end-2]
-
- # Proxy server URL for connection.
- # This determines where the socket is opened.
- set phost [lindex $args end-1]
- set pport [lindex $args end]
- if {[string first : $phost] != -1} {
- # IPv6 address, wrap it in [] so we can append :pport
- set phost "\[${phost}\]"
- }
- set url http://${phost}:${pport}
- # Elements of args other than host and port are not used when
- # AsyncTransaction opens a socket. Those elements are -async and the
- # -type $tokenName for the https transaction. Option -async is used by
- # AsyncTransaction anyway, and -type $tokenName should not be propagated:
- # the proxy request adds its own -type value.
-
- set targ [lsearch -exact $args -type]
- if {$targ != -1} {
- # Record in the token that this is a proxy call.
- set token [lindex $args $targ+1]
- upvar 0 ${token} state
- set tim $state(-timeout)
- set state(proxyUsed) SecureProxyFailed
- # This value is overwritten with "SecureProxy" below if the CONNECT is
- # successful. If it is unsuccessful, the socket will be closed
- # below, and so in this unsuccessful case there are no other transactions
- # whose (proxyUsed) must be updated.
- } else {
- set tim 0
- }
- if {$tim == 0} {
- # Do not use infinite timeout for the proxy.
- set tim 30000
- }
-
- # Prepare and send a CONNECT request to the proxy, using
- # code similar to http::geturl.
- set requestHeaders [list Host $host]
- lappend requestHeaders Connection keep-alive
- if {$http(-proxyauth) != {}} {
- lappend requestHeaders Proxy-Authorization $http(-proxyauth)
- }
-
- set token2 [CreateToken $url -keepalive 0 -timeout $tim \
- -headers $requestHeaders -command [list http::AllDone $varName]]
- variable $token2
- upvar 0 $token2 state2
-
- # Kludges:
- # Setting this variable overrides the HTTP request line and also allows
- # -headers to override the Connection: header set by -keepalive.
- # The arguments "-keepalive 0" ensure that when Finish is called for an
- # unsuccessful request, the socket is always closed.
- set state2(bypass) "CONNECT $host:$port HTTP/1.1"
-
- AsyncTransaction $token2
-
- if {[info coroutine] ne {}} {
- # All callers in the http package are coroutines launched by
- # the event loop.
- # The cwait command requires a coroutine because it yields
- # to the caller; $varName is traced and the coroutine resumes
- # when the variable is written.
- cwait $varName
- } else {
- return -code error {code must run in a coroutine}
- # For testing with a non-coroutine caller outside the http package.
- # vwait $varName
- }
- unset $varName
-
- if { ($state2(state) ne "complete")
- || ($state2(status) ne "ok")
- || (![string is integer -strict $state2(responseCode)])
- } {
- set msg {the HTTP request to the proxy server did not return a valid\
- and complete response}
- if {[info exists state2(error)]} {
- append msg ": " [lindex $state2(error) 0]
- }
- cleanup $token2
- return -code error $msg
- }
-
- set code $state2(responseCode)
-
- if {($code >= 200) && ($code < 300)} {
- # All OK. The caller in package tls will now call "tls::import $sock".
- # The cleanup command does not close $sock.
- # Other tidying was done in http::Event.
-
- # If this is a persistent socket, any other transactions that are
- # already marked to use the socket will have their (proxyUsed) updated
- # when http::OpenSocket calls http::ConfigureNewSocket.
- set state(proxyUsed) SecureProxy
- set sock $state2(sock)
- cleanup $token2
- return $sock
- }
-
- if {$targ != -1} {
- # Non-OK HTTP status code; token is known because option -type
- # (cf. targ) was passed through tcltls, and so the useful
- # parts of the proxy's response can be copied to state(*).
- # Do not copy state2(sock).
- # Return the proxy response to the caller of geturl.
- foreach name $failedProxyValues {
- if {[info exists state2($name)]} {
- set state($name) $state2($name)
- }
- }
- set state(connection) close
- set msg "proxy connect failed: $code"
- # - This error message will be detected by http::OpenSocket and will
- # cause it to present the proxy's HTTP response as that of the
- # original $token transaction, identified only by state(proxyUsed)
- # as the response of the proxy.
- # - The cases where this would mislead the caller of http::geturl are
- # given a different value of msg (below) so that http::OpenSocket will
- # treat them as errors, but will preserve the $token array for
- # inspection by the caller.
- # - Status code 305 (Proxy Required) was deprecated for security reasons
- # in RFC 2616 (June 1999) and in any case should never be served by a
- # proxy.
- # - Other 3xx responses from the proxy are inappropriate, and should not
- # occur.
- # - A 401 response from the proxy is inappropriate, and should not
- # occur. It would be confusing if returned to the caller.
-
- if {($code >= 300) && ($code < 400)} {
- set msg "the proxy server responded to the HTTP request with an\
- inappropriate $code redirect"
- set loc [responseHeaderValue $token2 location]
- if {$loc ne {}} {
- append msg "to " $loc
- }
- } elseif {($code == 401)} {
- set msg "the proxy server responded to the HTTP request with an\
- inappropriate 401 request for target-host credentials"
- } else {
- }
- } else {
- set msg "connection to proxy failed with status code $code"
- }
-
- # - ${token2}(sock) has already been closed because -keepalive 0.
- # - Error return does not pass the socket ID to the
- # $token transaction, which retains its socket placeholder.
- cleanup $token2
- return -code error $msg
-}
-
-proc http::AllDone {varName args} {
- set $varName done
- return
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc http::socket
-# ------------------------------------------------------------------------------
-# This command is a drop-in replacement for ::socket.
-# Arguments and return value as for ::socket.
-#
-# Notes.
-# - http::socket is specified in place of ::socket by the definition of urlTypes
-# in the namespace header of this file (http.tcl).
-# - The command makes a simple call to ::socket unless the user has called
-# http::config to change the value of -threadlevel from the default value 0.
-# - For -threadlevel 1 or 2, if the Thread package is available, the command
-# waits in the event loop while the socket is opened in another thread. This
-# is a workaround for bug [824251] - it prevents http::geturl from blocking
-# the event loop if the DNS lookup or server connection is slow.
-# - FIXME Use a thread pool if connections are very frequent.
-# - FIXME The peer thread can transfer the socket only to the main interpreter
-# in the present thread. Therefore this code works only if this script runs
-# in the main interpreter. In a child interpreter, the parent must alias a
-# command to ::http::socket in the child, run http::socket in the parent,
-# and then transfer the socket to the child.
-# - The http::socket command is simple, and can easily be replaced with an
-# alternative command that uses a different technique to open a socket while
-# entering the event loop.
-# - Unexpected behaviour by thread::send -async (Thread 2.8.6).
-# An error in thread::send -async causes return of just the error message
-# (not the expected 3 elements), and raises a bgerror in the main thread.
-# Hence wrap the command with catch as a precaution.
-# ------------------------------------------------------------------------------
-
-proc http::socket {args} {
- variable ThreadVar
- variable ThreadCounter
- variable http
-
- LoadThreadIfNeeded
-
- set targ [lsearch -exact $args -type]
- if {$targ != -1} {
- set token [lindex $args $targ+1]
- set args [lreplace $args $targ $targ+1]
- upvar 0 $token state
- }
-
- if {!$http(usingThread)} {
- # Use plain "::socket". This is the default.
- return [eval ::socket $args]
- }
-
- set defcmd ::socket
- set sockargs $args
- set script "
- set code \[catch {
- [list proc ::SockInThread {caller defcmd sockargs} [info body ::http::SockInThread]]
- [list ::SockInThread [thread::id] $defcmd $sockargs]
- } result opts\]
- list \$code \$opts \$result
- "
-
- set state(tid) [thread::create]
- set varName ::http::ThreadVar([incr ThreadCounter])
- thread::send -async $state(tid) $script $varName
- Log >T Thread Start Wait $args -- coro [info coroutine] $varName
- if {[info coroutine] ne {}} {
- # All callers in the http package are coroutines launched by
- # the event loop.
- # The cwait command requires a coroutine because it yields
- # to the caller; $varName is traced and the coroutine resumes
- # when the variable is written.
- cwait $varName
- } else {
- return -code error {code must run in a coroutine}
- # For testing with a non-coroutine caller outside the http package.
- # vwait $varName
- }
- Log >U Thread End Wait $args -- coro [info coroutine] $varName [set $varName]
- thread::release $state(tid)
- set state(tid) {}
- set result [set $varName]
- unset $varName
- if {(![string is list $result]) || ([llength $result] != 3)} {
- return -code error "result from peer thread is not a list of\
- length 3: it is \n$result"
- }
- lassign $result threadCode threadDict threadResult
- if {($threadCode != 0)} {
- # This is an error in thread::send. Return the lot.
- return -options $threadDict -code error $threadResult
- }
-
- # Now the results of the catch in the peer thread.
- lassign $threadResult catchCode errdict sock
-
- if {($catchCode == 0) && ($sock ni [chan names])} {
- return -code error {Transfer of socket from peer thread failed.\
- Check that this script is not running in a child interpreter.}
- }
- return -options $errdict -code $catchCode $sock
-}
-
-# The commands below are dependencies of http::socket and
-# http::SecureProxyConnect and are not used elsewhere.
-
-# ------------------------------------------------------------------------------
-# Proc http::LoadThreadIfNeeded
-# ------------------------------------------------------------------------------
-# Command to load the Thread package if it is needed. If it is needed and not
-# loadable, the outcome depends on $http(-threadlevel):
-# value 0 => Thread package not required, no problem
-# value 1 => operate as if -threadlevel 0
-# value 2 => error return
-#
-# Arguments: none
-# Return Value: none
-# ------------------------------------------------------------------------------
-
-proc http::LoadThreadIfNeeded {} {
- variable http
- if {$http(usingThread) || ($http(-threadlevel) == 0)} {
- return
- }
- if {[catch {package require Thread}]} {
- if {$http(-threadlevel) == 2} {
- set msg {[http::config -threadlevel] has value 2,\
- but the Thread package is not available}
- return -code error $msg
- }
- return
- }
- set http(usingThread) 1
- return
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc http::SockInThread
-# ------------------------------------------------------------------------------
-# Command http::socket is a ::socket replacement. It defines and runs this
-# command, http::SockInThread, in a peer thread.
-#
-# Arguments:
-# caller
-# defcmd
-# sockargs
-#
-# Return value: list of values that describe the outcome. The return is
-# intended to be a normal (non-error) return in all cases.
-# ------------------------------------------------------------------------------
-
-proc http::SockInThread {caller defcmd sockargs} {
- package require Thread
-
- set catchCode [catch {eval $defcmd $sockargs} sock errdict]
- if {$catchCode == 0} {
- set catchCode [catch {thread::transfer $caller $sock; set sock} sock errdict]
- }
- return [list $catchCode $errdict $sock]
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc http::cwaiter::cwait
-# ------------------------------------------------------------------------------
-# Command to substitute for vwait, without the ordering issues.
-# A command that uses cwait must be a coroutine that is launched by an event,
-# e.g. fileevent or after idle, and has no calling code to be resumed upon
-# "yield". It cannot return a value.
-#
-# Arguments:
-# varName - fully-qualified name of the variable that the calling script
-# will write to resume the coroutine. Any scalar variable or
-# array element is permitted.
-# coroName - (optional) name of the coroutine to be called when varName is
-# written - defaults to this coroutine
-# timeout - (optional) timeout value in ms
-# timeoutValue - (optional) value to assign to varName if there is a timeout
-#
-# Return Value: none
-# ------------------------------------------------------------------------------
-
-namespace eval http::cwaiter {
- namespace export cwait
- variable log {}
- variable logOn 0
-}
-
-proc http::cwaiter::cwait {
- varName {coroName {}} {timeout {}} {timeoutValue {}}
-} {
- set thisCoro [info coroutine]
- if {$thisCoro eq {}} {
- return -code error {cwait cannot be called outside a coroutine}
- }
- if {$coroName eq {}} {
- set coroName $thisCoro
- }
- if {[string range $varName 0 1] ne {::}} {
- return -code error {argument varName must be fully qualified}
- }
- if {$timeout eq {}} {
- set toe {}
- } elseif {[string is integer -strict $timeout] && ($timeout > 0)} {
- set toe [after $timeout [list set $varName $timeoutValue]]
- } else {
- return -code error {if timeout is supplied it must be a positive integer}
- }
-
- set cmd [list ::http::cwaiter::CwaitHelper $varName $coroName $toe]
- trace add variable $varName write $cmd
- CoLog "Yield $varName $coroName"
- yield
- CoLog "Resume $varName $coroName"
- return
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc http::cwaiter::CwaitHelper
-# ------------------------------------------------------------------------------
-# Helper command called by the trace set by cwait.
-# - Ignores the arguments added by trace.
-# - A simple call to $coroName works, and in error cases gives a suitable stack
-# trace, but because it is inside a trace the headline error message is
-# something like {can't set "::Result(6)": error}, not the actual
-# error. So let the trace command return.
-# - Remove the trace immediately. We don't want multiple calls.
-# ------------------------------------------------------------------------------
-
-proc http::cwaiter::CwaitHelper {varName coroName toe args} {
- CoLog "got $varName for $coroName"
- set cmd [list ::http::cwaiter::CwaitHelper $varName $coroName $toe]
- trace remove variable $varName write $cmd
- after cancel $toe
-
- after 0 $coroName
- return
-}
-
-
-# ------------------------------------------------------------------------------
-# Proc http::cwaiter::LogInit
-# ------------------------------------------------------------------------------
-# Call this command to initiate debug logging and clear the log.
-# ------------------------------------------------------------------------------
-
-proc http::cwaiter::LogInit {} {
- variable log
- variable logOn
- set log {}
- set logOn 1
- return
-}
-
-proc http::cwaiter::LogRead {} {
- variable log
- return $log
-}
-
-proc http::cwaiter::CoLog {msg} {
- variable log
- variable logOn
- if {$logOn} {
- append log $msg \n
- }
- return
-}
-
-namespace eval http {
- namespace import ::http::cwaiter::*
-}
-
-# Local variables:
-# indent-tabs-mode: t
-# End:
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/logger-0.9.5.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/logger-0.9.5.tm
deleted file mode 100644
index 739e1c91..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/logger-0.9.5.tm
+++ /dev/null
@@ -1,1297 +0,0 @@
-# logger.tcl --
-#
-# Tcl implementation of a general logging facility.
-#
-# Copyright (c) 2003 by David N. Welton
-# Copyright (c) 2004-2011 by Michael Schlenker
-# Copyright (c) 2006,2015 by Andreas Kupries
-#
-# See the file license.terms.
-
-# The logger package provides an 'object oriented' log facility that
-# lets you have trees of services, that inherit from one another.
-# This is accomplished through the use of Tcl namespaces.
-
-
-package require Tcl 8.5 9
-package provide logger 0.9.5
-
-namespace eval ::logger {
- namespace eval tree {}
- namespace export init enable disable services servicecmd import
-
- # The active services.
- variable services {}
-
- # The log 'levels'.
- variable levels [list debug info notice warn error critical alert emergency]
-
- # The default global log level used for new logging services
- variable enabled "debug"
-
- # Tcl return codes (in numeric order)
- variable RETURN_CODES [list "ok" "error" "return" "break" "continue"]
-}
-
-# Try to load msgcat and fall back to format if it fails
-if {[catch {package require msgcat}]} {
- interp alias {} ::logger::mc {} ::format
-} else {
- namespace eval ::logger {
- namespace import ::msgcat::mc
- }
-}
-
-# ::logger::_nsExists --
-#
-# Workaround for missing namespace exists in Tcl 8.2 and 8.3.
-#
-
-if {[package vcompare [package provide Tcl] 8.4] < 0} {
- proc ::logger::_nsExists {ns} {
- expr {![catch {namespace parent $ns}]}
- }
-} else {
- proc ::logger::_nsExists {ns} {
- namespace exists $ns
- }
-}
-
-# ::logger::_cmdPrefixExists --
-#
-# Utility function to check if a given callback prefix exists,
-# this should catch all oddities in prefix names, including spaces,
-# glob patterns, non normalized namespaces etc.
-#
-# Arguments:
-# prefix - The command prefix to check
-#
-# Results:
-# 1 or 0 for yes or no
-#
-proc ::logger::_cmdPrefixExists {prefix} {
- set cmd [lindex $prefix 0]
- set full [namespace eval :: namespace which [list $cmd]]
- if {[string equal $full ""]} {return 0} else {return 1}
- # normalize namespaces
- set ns [namespace qualifiers $cmd]
- set cmd ${ns}::[namespace tail $cmd]
- set matches [::info commands ${ns}::*]
- if {[lsearch -exact $matches $cmd] != -1} {return 1}
- return 0
-}
-
-# ::logger::walk --
-#
-# Walk namespaces, starting in 'start', and evaluate 'code' in
-# them.
-#
-# Arguments:
-# start - namespace to start in.
-# code - code to execute in namespaces walked.
-#
-# Side Effects:
-# Side effects of code executed.
-#
-# Results:
-# None.
-
-proc ::logger::walk { start code } {
- set children [namespace children $start]
- foreach c $children {
- logger::walk $c $code
- namespace eval $c $code
- }
-}
-
-proc ::logger::init {service} {
- variable levels
- variable services
- variable enabled
-
- if {[string length [string trim $service {:}]] == 0} {
- return -code error \
- -errorcode [list LOGGER EMPTY_SERVICENAME] \
- [::logger::mc "Service name invalid. May not consist only of : or be empty"]
- }
- # We create a 'tree' namespace to house all the services, so
- # they are in a 'safe' namespace sandbox, and won't overwrite
- # any commands.
- namespace eval tree::${service} {
- variable service
- variable levels
- variable oldname
- variable enabled
- }
-
- lappend services $service
-
- set [namespace current]::tree::${service}::service $service
- set [namespace current]::tree::${service}::levels $levels
- set [namespace current]::tree::${service}::oldname $service
- set [namespace current]::tree::${service}::enabled $enabled
-
- namespace eval tree::${service} {
- # Callback to use when the service in question is shut down.
- variable delcallback [namespace current]::no-op
-
- # Callback when the loglevel is changed
- variable levelchangecallback [namespace current]::no-op
-
- # State variable to decide when to call levelcallback
- variable inSetLevel 0
-
- # The currently configured levelcommands
- variable lvlcmds
- array set lvlcmds {}
-
- # List of procedures registered via the trace command
- variable traceList ""
-
- # Flag indicating whether or not tracing is currently enabled
- variable tracingEnabled 0
-
- # We use this to disable a service completely. In Tcl 8.4
- # or greater, by using this, disabled log calls are a
- # no-op!
-
- proc no-op args {}
-
- proc stdoutcmd {level text} {
- variable service
- puts "\[[clock format [clock seconds]]\] \[$service\] \[$level\] \'$text\'"
- }
-
- proc stderrcmd {level text} {
- variable service
- puts stderr "\[[clock format [clock seconds]]\] \[$service\] \[$level\] \'$text\'"
- }
-
-
- # setlevel --
- #
- # This command differs from enable and disable in that
- # it disables all the levels below that selected, and
- # then enables all levels above it, which enable/disable
- # do not do.
- #
- # Arguments:
- # lv - the level, as defined in $levels.
- #
- # Side Effects:
- # Runs disable for the level, and then enable, in order
- # to ensure that all levels are set correctly.
- #
- # Results:
- # None.
-
-
- proc setlevel {lv} {
- variable inSetLevel 1
- set oldlvl [currentloglevel]
-
- # do not allow enable and disable to do recursion
- if {[catch {
- disable $lv 0
- set newlvl [enable $lv 0]
- } msg] == 1} {
- return -code error -errorcode $::errorCode $msg
- }
- # do the recursion here
- logger::walk [namespace current] [list setlevel $lv]
-
- set inSetLevel 0
- lvlchangewrapper $oldlvl $newlvl
- return
- }
-
- # enable --
- #
- # Enable a particular 'level', and above, for the
- # service, and its 'children'.
- #
- # Arguments:
- # lv - the level, as defined in $levels.
- #
- # Side Effects:
- # Enables logging for the particular level, and all
- # above it (those more important). It also walks
- # through all services that are 'children' and enables
- # them at the same level or above.
- #
- # Results:
- # None.
-
- proc enable {lv {recursion 1}} {
- variable levels
- set lvnum [lsearch -exact $levels $lv]
- if { $lvnum == -1 } {
- return -code error \
- -errorcode [list LOGGER INVALID_LEVEL] \
- [::logger::mc "Invalid level '%s' - levels are %s" $lv $levels]
- }
-
- variable enabled
- set newlevel $enabled
- set elnum [lsearch -exact $levels $enabled]
- if {($elnum == -1) || ($elnum > $lvnum)} {
- set newlevel $lv
- }
-
- variable service
- while { $lvnum < [llength $levels] } {
- interp alias {} [namespace current]::[lindex $levels $lvnum] \
- {} [namespace current]::[lindex $levels $lvnum]cmd
- incr lvnum
- }
-
- if {$recursion} {
- logger::walk [namespace current] [list enable $lv]
- }
- lvlchangewrapper $enabled $newlevel
- set enabled $newlevel
- }
-
- # disable --
- #
- # Disable a particular 'level', and below, for the
- # service, and its 'children'.
- #
- # Arguments:
- # lv - the level, as defined in $levels.
- #
- # Side Effects:
- # Disables logging for the particular level, and all
- # below it (those less important). It also walks
- # through all services that are 'children' and disables
- # them at the same level or below.
- #
- # Results:
- # None.
-
- proc disable {lv {recursion 1}} {
- variable levels
- set lvnum [lsearch -exact $levels $lv]
- if { $lvnum == -1 } {
- return -code error \
- -errorcode [list LOGGER INVALID_LEVEL] \
- [::logger::mc "Invalid level '%s' - levels are %s" $lv $levels]
- }
-
- variable enabled
- set newlevel $enabled
- set elnum [lsearch -exact $levels $enabled]
- if {($elnum > -1) && ($elnum <= $lvnum)} {
- if {$lvnum+1 >= [llength $levels]} {
- set newlevel "none"
- } else {
- set newlevel [lindex $levels [expr {$lvnum+1}]]
- }
- }
-
- while { $lvnum >= 0 } {
-
- interp alias {} [namespace current]::[lindex $levels $lvnum] {} \
- [namespace current]::no-op
- incr lvnum -1
- }
- if {$recursion} {
- logger::walk [namespace current] [list disable $lv]
- }
- lvlchangewrapper $enabled $newlevel
- set enabled $newlevel
- }
-
- # currentloglevel --
- #
- # Get the currently enabled log level for this service.
- #
- # Arguments:
- # none
- #
- # Side Effects:
- # none
- #
- # Results:
- # current log level
- #
-
- proc currentloglevel {} {
- variable enabled
- return $enabled
- }
-
- # lvlchangeproc --
- #
- # Set or introspect a callback for when the logger instance
- # changes its loglevel.
- #
- # Arguments:
- # cmd - the Tcl command to call, it is called with two parameters, old and new log level.
- # or none for introspection
- #
- # Side Effects:
- # None.
- #
- # Results:
- # If no arguments are given return the current callback cmd.
-
- proc lvlchangeproc {args} {
- variable levelchangecallback
-
- switch -exact -- [llength [::info level 0]] {
- 1 {return $levelchangecallback}
- 2 {
- if {[::logger::_cmdPrefixExists [lindex $args 0]]} {
- set levelchangecallback [lindex $args 0]
- } else {
- return -code error \
- -errorcode [list LOGGER INVALID_CMD] \
- [::logger::mc "Invalid cmd '%s' - does not exist" [lindex $args 0]]
- }
- }
- default {
- return -code error \
- -errorcode [list LOGGER WRONG_NUM_ARGS] \
- [::logger::mc "Wrong # of arguments. Usage: \${log}::lvlchangeproc ?cmd?"]
- }
- }
- }
-
- proc lvlchangewrapper {old new} {
- variable inSetLevel
-
- # we are called after disable and enable are finished
- if {$inSetLevel} {return}
-
- # no action if level does not change
- if {[string equal $old $new]} {return}
-
- variable levelchangecallback
- # no action if levelchangecallback isn't a valid command
- if {[::logger::_cmdPrefixExists $levelchangecallback]} {
- catch {
- uplevel \#0 [linsert $levelchangecallback end $old $new]
- }
- }
- }
-
- # logproc --
- #
- # Command used to create a procedure that is executed to
- # perform the logging. This could write to disk, out to
- # the network, or something else.
- # If two arguments are given, use an existing command.
- # If three arguments are given, create a proc.
- #
- # Arguments:
- # lv - the level to log, which must be one of $levels.
- # args - either zero, one or two arguments.
- # if zero this returns the current command registered
- # if one, this is a cmd name that is called for this level
- # if two, these are an argument and proc body
- #
- # Side Effects:
- # Creates a logging command to take care of the details
- # of logging an event.
- #
- # Results:
- # If called with zero length args, returns the name of the currently
- # configured logging procedure.
- #
- #
-
- proc logproc {lv args} {
- variable levels
- variable lvlcmds
-
- set lvnum [lsearch -exact $levels $lv]
- if { ($lvnum == -1) && ($lv != "trace") } {
- return -code error \
- -errorcode [list LOGGER INVALID_LEVEL] \
- [::logger::mc "Invalid level '%s' - levels are %s" $lv $levels]
- }
- switch -exact -- [llength $args] {
- 0 {
- return $lvlcmds($lv)
- }
- 1 {
- set cmd [lindex $args 0]
- if {[string equal "[namespace current]::${lv}cmd" $cmd]} {return}
- if {[llength [::info commands $cmd]]} {
- proc ${lv}cmd args [format {
- uplevel 1 [list %s [expr {[llength $args]==1 ? [lindex $args end] : $args}]]
- } $cmd]
- } else {
- return -code error \
- -errorcode [list LOGGER INVALID_CMD] \
- [::logger::mc "Invalid cmd '%s' - does not exist" $cmd]
- }
- set lvlcmds($lv) $cmd
- }
- 2 {
- foreach {arg body} $args {break}
- proc ${lv}cmd args [format {\
- _setservicename args
- set val [%s [expr {[llength $args]==1 ? [lindex $args end] : $args}]]
- _restoreservice
- set val} ${lv}customcmd]
- proc ${lv}customcmd $arg $body
- set lvlcmds($lv) [namespace current]::${lv}customcmd
- }
- default {
- return -code error \
- -errorcode [list LOGGER WRONG_USAGE] \
- [::logger::mc \
- "Usage: \${log}::logproc level ?cmd?\nor \${log}::logproc level argname body" ]
- }
- }
- }
-
-
- # delproc --
- #
- # Set or introspect a callback for when the logger instance
- # is deleted.
- #
- # Arguments:
- # cmd - the Tcl command to call.
- # or none for introspection
- #
- # Side Effects:
- # None.
- #
- # Results:
- # If no arguments are given return the current callback cmd.
-
- proc delproc {args} {
- variable delcallback
-
- switch -exact -- [llength [::info level 0]] {
- 1 {return $delcallback}
- 2 { if {[::logger::_cmdPrefixExists [lindex $args 0]]} {
- set delcallback [lindex $args 0]
- } else {
- return -code error \
- -errorcode [list LOGGER INVALID_CMD] \
- [::logger::mc "Invalid cmd '%s' - does not exist" [lindex $args 0]]
- }
- }
- default {
- return -code error \
- -errorcode [list LOGGER WRONG_NUM_ARGS] \
- [::logger::mc "Wrong # of arguments. Usage: \${log}::delproc ?cmd?"]
- }
- }
- }
-
-
- # delete --
- #
- # Delete the namespace and its children.
-
- proc delete {} {
- variable delcallback
- variable service
-
- logger::walk [namespace current] delete
- if {[::logger::_cmdPrefixExists $delcallback]} {
- uplevel \#0 [lrange $delcallback 0 end]
- }
- # clean up the global services list
- set idx [lsearch -exact [logger::services] $service]
- if {$idx !=-1} {
- set ::logger::services [lreplace [logger::services] $idx $idx]
- }
-
- namespace delete [namespace current]
-
- }
-
- # services --
- #
- # Return all child services
-
- proc services {} {
- variable service
-
- set children [list]
- foreach srv [logger::services] {
- if {[string match "${service}::*" $srv]} {
- lappend children $srv
- }
- }
- return $children
- }
-
- # servicename --
- #
- # Return the name of the service
-
- proc servicename {} {
- variable service
- return $service
- }
-
- proc _setservicename {argname} {
- variable service
- variable oldname
- upvar 1 $argname arg
- if {[llength $arg] <= 1} {
- return
- }
-
- set count -1
- set newname ""
- while {[string equal [lindex $arg [expr {$count+1}]] "-_logger::service"]} {
- incr count 2
- set newname [lindex $arg $count]
- }
- if {[string equal $newname ""]} {
- return
- }
- set oldname $service
- set service $newname
- # Pop off "-_logger::service " from argument list
- set arg [lreplace $arg 0 $count]
- }
-
- proc _restoreservice {} {
- variable service
- variable oldname
- set service $oldname
- return
- }
-
- proc trace { action args } {
- variable service
-
- # Allow other boolean values (true, false, yes, no, 0, 1) to be used
- # as synonymns for "on" and "off".
-
- if {[string is boolean $action]} {
- set xaction [expr {($action && 1) ? "on" : "off"}]
- } else {
- set xaction $action
- }
-
- # Check for required arguments for actions/subcommands and dispatch
- # to the appropriate procedure.
-
- switch -- $xaction {
- "status" {
- return [uplevel 1 [list logger::_trace_status $service $args]]
- }
- "on" {
- if {[llength $args]} {
- return -code error \
- -errorcode [list LOGGER WRONG_NUM_ARGS] \
- [::logger::mc "wrong # args: should be \"trace on\""]
- }
- return [logger::_trace_on $service]
- }
- "off" {
- if {[llength $args]} {
- return -code error \
- -errorcode [list LOGGER WRONG_NUM_ARGS] \
- [::logger::mc "wrong # args: should be \"trace off\""]
- }
- return [logger::_trace_off $service]
- }
- "add" {
- if {![llength $args]} {
- return -code error \
- -errorcode [list LOGGER WRONG_NUM_ARGS] \
- [::logger::mc "wrong # args: should be \"trace add ?-ns? ...\""]
- }
- return [uplevel 1 [list ::logger::_trace_add $service $args]]
- }
- "remove" {
- if {![llength $args]} {
- return -code error \
- -errorcode [list LOGGER WRONG_NUM_ARGS] \
- [::logger::mc "wrong # args: should be \"trace remove ?-ns? ...\""]
- }
- return [uplevel 1 [list ::logger::_trace_remove $service $args]]
- }
-
- default {
- return -code error \
- -errorcode [list LOGGER INVALID_ARG] \
- [::logger::mc "Invalid action \"%s\": must be status, add, remove,\
- on, or off" $action]
- }
- }
- }
-
- # Walk the parent service namespaces to see first, if they
- # exist, and if any are enabled, and then, as a
- # consequence, enable this one
- # too.
-
- enable $enabled
- variable parent [namespace parent]
- while {[string compare $parent "::logger::tree"]} {
- # If the 'enabled' variable doesn't exist, create the
- # whole thing.
- if { ! [::info exists ${parent}::enabled] } {
- logger::init [string range $parent 16 end]
- }
- set enabled [set ${parent}::enabled]
- enable $enabled
- set parent [namespace parent $parent]
- }
- }
-
- # Now create the commands for different levels.
-
- namespace eval tree::${service} {
- set parent [namespace parent]
-
- # We 'inherit' the commands from the parents. This
- # means that, if you want to share the same methods with
- # children, they should be instantiated after the parent's
- # methods have been defined.
-
- variable lvl ; # prevent creative writing to the global scope
- if {[string compare $parent "::logger::tree"]} {
- foreach lvl [::logger::levels] {
- # OPTIMIZE: do not allow multiple aliases in the hierarchy
- # they can always be replaced by more efficient
- # direct aliases to the target procs.
- interp alias {} [namespace current]::${lvl}cmd \
- {} ${parent}::${lvl}cmd -_logger::service $service
- }
- # inherit the starting loglevel of the parent service
- setlevel [${parent}::currentloglevel]
- } else {
- foreach lvl [concat [::logger::levels] "trace"] {
- proc ${lvl}cmd args [format {\
- _setservicename args
- set val [stdoutcmd %s [expr {[llength $args]==1 ? [lindex $args end] : $args}]]
- _restoreservice
- set val } $lvl]
-
- set lvlcmds($lvl) [namespace current]::${lvl}cmd
- }
- setlevel $::logger::enabled
- }
- unset lvl ; # drop the temp iteration variable
- }
-
- return ::logger::tree::${service}
-}
-
-# ::logger::services --
-#
-# Returns a list of all active services.
-#
-# Arguments:
-# None.
-#
-# Side Effects:
-# None.
-#
-# Results:
-# List of active services.
-
-proc ::logger::services {} {
- variable services
- return $services
-}
-
-# ::logger::enable --
-#
-# Global enable for a certain level. NOTE - this implementation
-# isn't terribly effective at the moment, because it might hit
-# children before their parents, who will then walk down the
-# tree attempting to disable the children again.
-#
-# Arguments:
-# lv - level above which to enable logging.
-#
-# Side Effects:
-# Enables logging in a given level, and all higher levels.
-#
-# Results:
-# None.
-
-proc ::logger::enable {lv} {
- variable services
- if {[catch {
- foreach sv $services {
- ::logger::tree::${sv}::enable $lv
- }
- } msg] == 1} {
- return -code error -errorcode $::errorCode $msg
- }
-}
-
-proc ::logger::disable {lv} {
- variable services
- if {[catch {
- foreach sv $services {
- ::logger::tree::${sv}::disable $lv
- }
- } msg] == 1} {
- return -code error -errorcode $::errorCode $msg
- }
-}
-
-proc ::logger::setlevel {lv} {
- variable services
- variable enabled
- variable levels
- if {[lsearch -exact $levels $lv] == -1} {
- return -code error \
- -errorcode [list LOGGER INVALID_LEVEL] \
- [::logger::mc "Invalid level '%s' - levels are %s" $lv $levels]
- }
- set enabled $lv
- if {[catch {
- foreach sv $services {
- ::logger::tree::${sv}::setlevel $lv
- }
- } msg] == 1} {
- return -code error -errorcode $::errorCode $msg
- }
-}
-
-# ::logger::levels --
-#
-# Introspect the available log levels. Provided so a caller does
-# not need to know implementation details or code the list
-# himself.
-#
-# Arguments:
-# None.
-#
-# Side Effects:
-# None.
-#
-# Results:
-# levels - The list of valid log levels accepted by enable and disable
-
-proc ::logger::levels {} {
- variable levels
- return $levels
-}
-
-# ::logger::servicecmd --
-#
-# Get the command token for a given service name.
-#
-# Arguments:
-# service - name of the service.
-#
-# Side Effects:
-# none
-#
-# Results:
-# log - namespace token for this service
-
-proc ::logger::servicecmd {service} {
- variable services
- if {[lsearch -exact $services $service] == -1} {
- return -code error \
- -errorcode [list LOGGER NO_SUCH_SERVICE] \
- [::logger::mc "Service \"%s\" does not exist." $service]
- }
- return "::logger::tree::${service}"
-}
-
-# ::logger::import --
-#
-# Import the logging commands.
-#
-# Arguments:
-# service - name of the service.
-#
-# Side Effects:
-# creates aliases in the target namespace
-#
-# Results:
-# none
-
-proc ::logger::import {args} {
- variable services
-
- if {[llength $args] == 0 || [llength $args] > 7} {
- return -code error \
- -errorcode [list LOGGER WRONG_NUM_ARGS] \
- [::logger::mc \
- "Wrong # of arguments: \"logger::import ?-all?\
- ?-force?\
- ?-prefix prefix? ?-namespace namespace? service\""]
- }
-
- # process options
- #
- set import_all 0
- set force 0
- set prefix ""
- set ns [uplevel 1 namespace current]
- while {[llength $args] > 1} {
- set opt [lindex $args 0]
- set args [lrange $args 1 end]
- switch -exact -- $opt {
- -all { set import_all 1}
- -prefix { set prefix [lindex $args 0]
- set args [lrange $args 1 end]
- }
- -namespace {
- set ns [lindex $args 0]
- set args [lrange $args 1 end]
- }
- -force {
- set force 1
- }
- default {
- return -code error \
- -errorcode [list LOGGER UNKNOWN_ARG] \
- [::logger::mc \
- "Unknown argument: \"%s\" :\nUsage:\
- \"logger::import ?-all? ?-force?\
- ?-prefix prefix? ?-namespace namespace? service\"" $opt]
- }
- }
- }
-
- #
- # build the list of commands to import
- #
-
- set cmds [logger::levels]
- lappend cmds "trace"
- if {$import_all} {
- lappend cmds setlevel enable disable logproc delproc services
- lappend cmds servicename currentloglevel delete
- }
-
- #
- # check the service argument
- #
-
- set service [lindex $args 0]
- if {[lsearch -exact $services $service] == -1} {
- return -code error \
- -errorcode [list LOGGER NO_SUCH_SERVICE] \
- [::logger::mc "Service \"%s\" does not exist." $service]
- }
-
- #
- # setup the namespace for the import
- #
-
- set sourcens [logger::servicecmd $service]
- set localns [uplevel 1 namespace current]
-
- if {[string match ::* $ns]} {
- set importns $ns
- } else {
- set importns ${localns}::$ns
- }
-
- # fake namespace exists for Tcl 8.2 - 8.3
- if {![_nsExists $importns]} {
- namespace eval $importns {}
- }
-
-
- #
- # prepare the import
- #
-
- set imports ""
- foreach cmd $cmds {
- set cmdname ${importns}::${prefix}$cmd
- set collision [llength [info commands $cmdname]]
- if {$collision && !$force} {
- return -code error \
- -errorcode [list LOGGER IMPORT_NAME_EXISTS] \
- [::logger::mc "can't import command \"%s\": already exists" $cmdname]
- }
- lappend imports ${importns}::${prefix}$cmd ${sourcens}::${cmd}
- }
-
- #
- # and execute the aliasing after checking all is well
- #
-
- foreach {target source} $imports {
- proc $target {args} "uplevel 1 \[linsert \$args 0 $source \]"
- }
-}
-
-# ::logger::initNamespace --
-#
-# Creates a logger for the specified namespace and makes the log
-# commands available to said namespace as well. Allows the initial
-# setting of a default log level.
-#
-# Arguments:
-# ns - Namespace to initialize, is also the service name, modulo a ::-prefix
-# level - Initial log level, optional, defaults to 'warn'.
-#
-# Side Effects:
-# creates aliases in the target namespace
-#
-# Results:
-# none
-
-proc ::logger::initNamespace {ns {level {}}} {
- set service [string trimleft $ns :]
- if {$level == ""} {
- # No user-specified level. Figure something out.
- # - If the parent service exists then the 'logger::init'
- # below will automatically inherit its level. Good enough.
- # - Without a parent service go and use a default level of 'warn'.
- set parent [string trimleft [namespace qualifiers $service] :]
- set hasparent [expr {($parent != {}) && [_nsExists ::logger::tree::${parent}]}]
- if {!$hasparent} {
- set level warn
- }
- }
-
- namespace eval $ns [list ::logger::init $service]
- namespace eval $ns [list ::logger::import -force -all -namespace log $service]
- if {$level != ""} {
- namespace eval $ns [list log::setlevel $level]
- }
- return
-}
-
-# This procedure handles the "logger::trace status" command. Given no
-# arguments, returns a list of all procedures that have been registered
-# via "logger::trace add". Given one or more procedure names, it will
-# return 1 if all were registered, or 0 if any were not.
-
-proc ::logger::_trace_status { service procList } {
- upvar #0 ::logger::tree::${service}::traceList traceList
-
- # If no procedure names were given, just return the registered list
-
- if {![llength $procList]} {
- return $traceList
- }
-
- # Get caller's namespace for qualifying unqualified procedure names
-
- set caller_ns [uplevel 1 namespace current]
- set caller_ns [string trimright $caller_ns ":"]
-
- # Search for any specified proc names that are *not* registered
-
- foreach procName $procList {
- # Make sure the procedure namespace is qualified
-
- if {![string match "::*" $procName]} {
- set procName ${caller_ns}::$procName
- }
-
- # Check if the procedure has been registered for tracing
-
- if {[lsearch -exact $traceList $procName] == -1} {
- return 0
- }
- }
-
- return 1
-}
-
-# This procedure handles the "logger::trace on" command. If tracing
-# is turned off, it will enable Tcl trace handlers for all of the procedures
-# registered via "logger::trace add". Does nothing if tracing is already
-# turned on.
-
-proc ::logger::_trace_on { service } {
- set tcl_version [package provide Tcl]
-
- if {[package vcompare $tcl_version "8.4"] < 0} {
- return -code error \
- -errorcode [list LOGGER TRACE_NOT_AVAILABLE] \
- [::logger::mc "execution tracing is not available in Tcl %s" $tcl_version]
- }
-
- namespace eval ::logger::tree::${service} {
- if {!$tracingEnabled} {
- set tracingEnabled 1
- ::logger::_enable_traces $service $traceList
- }
- }
-
- return 1
-}
-
-# This procedure handles the "logger::trace off" command. If tracing
-# is turned on, it will disable Tcl trace handlers for all of the procedures
-# registered via "logger::trace add", leaving them in the list so they
-# tracing on all of them can be enabled again with "logger::trace on".
-# Does nothing if tracing is already turned off.
-
-proc ::logger::_trace_off { service } {
- namespace eval ::logger::tree::${service} {
- if {$tracingEnabled} {
- ::logger::_disable_traces $service $traceList
- set tracingEnabled 0
- }
- }
-
- return 1
-}
-
-# This procedure is used by the logger::trace add and remove commands to
-# process the arguments in a common fashion. If the -ns switch is given
-# first, this procedure will return a list of all existing procedures in
-# all of the namespaces given in remaining arguments. Otherwise, each
-# argument is taken to be either a pattern for a glob-style search of
-# procedure names or, failing that, a namespace, in which case this
-# procedure returns a list of all the procedures matching the given
-# pattern (or all in the named namespace, if no procedures match).
-
-proc ::logger::_trace_get_proclist { inputList } {
- set procList ""
-
- if {[string equal [lindex $inputList 0] "-ns"]} {
- # Verify that at least one target namespace was supplied
-
- set inputList [lrange $inputList 1 end]
- if {![llength $inputList]} {
- return -code error \
- -errorcode [list LOGGER TARGET_MISSING] \
- [::logger::mc "Must specify at least one namespace target"]
- }
-
- # Rebuild the argument list to contain namespace procedures
-
- foreach namespace $inputList {
- # Don't allow tracing of the logger (or child) namespaces
-
- if {![string match "::logger::*" $namespace]} {
- set nsProcList [::info procs ${namespace}::*]
- set procList [concat $procList $nsProcList]
- }
- }
- } else {
- # Search for procs or namespaces matching each of the specified
- # patterns.
-
- foreach pattern $inputList {
- set matches [uplevel 1 ::info proc $pattern]
-
- if {![llength $matches]} {
- if {[uplevel 1 namespace exists $pattern]} {
- set matches [::info procs ${pattern}::*]
- }
-
- # Matched procs will be qualified due to above pattern
-
- set procList [concat $procList $matches]
- } elseif {[string match "::*" $pattern]} {
- # Patterns were pre-qualified - add them directly
-
- set procList [concat $procList $matches]
- } else {
- # Qualify each proc with the namespace it was in
-
- set ns [uplevel 1 namespace current]
- if {$ns == "::"} {
- set ns ""
- }
- foreach proc $matches {
- lappend procList ${ns}::$proc
- }
- }
- }
- }
-
- return $procList
-}
-
-# This procedure handles the "logger::trace add" command. If the tracing
-# feature is enabled, it will enable the Tcl entry and leave trace handlers
-# for each procedure specified that isn't already being traced. Each
-# procedure is added to the list of procedures that the logger trace feature
-# should log when tracing is enabled.
-
-proc ::logger::_trace_add { service procList } {
- upvar #0 ::logger::tree::${service}::traceList traceList
-
- # Handle -ns switch and glob search patterns for procedure names
-
- set procList [uplevel 1 [list logger::_trace_get_proclist $procList]]
-
- # Enable tracing for each procedure that has not previously been
- # specified via logger::trace add. If tracing is off, this will just
- # store the name of the procedure for later when tracing is turned on.
-
- foreach procName $procList {
- if {[lsearch -exact $traceList $procName] == -1} {
- lappend traceList $procName
- ::logger::_enable_traces $service [list $procName]
- }
- }
-}
-
-# This procedure handles the "logger::trace remove" command. If the tracing
-# feature is enabled, it will remove the Tcl entry and leave trace handlers
-# for each procedure specified. Each procedure is removed from the list
-# of procedures that the logger trace feature should log when tracing is
-# enabled.
-
-proc ::logger::_trace_remove { service procList } {
- upvar #0 ::logger::tree::${service}::traceList traceList
-
- # Handle -ns switch and glob search patterns for procedure names
-
- set procList [uplevel 1 [list logger::_trace_get_proclist $procList]]
-
- # Disable tracing for each proc that previously had been specified
- # via logger::trace add. If tracing is off, this will just
- # remove the name of the procedure from the trace list so that it
- # will be excluded when tracing is turned on.
-
- foreach procName $procList {
- set index [lsearch -exact $traceList $procName]
- if {$index != -1} {
- set traceList [lreplace $traceList $index $index]
- ::logger::_disable_traces $service [list $procName]
- }
- }
-}
-
-# This procedure enables Tcl trace handlers for all procedures specified.
-# It is used both to enable Tcl's tracing for a single procedure when
-# removed via "logger::trace add", as well as to enable all traces
-# via "logger::trace on".
-
-proc ::logger::_enable_traces { service procList } {
- upvar #0 ::logger::tree::${service}::tracingEnabled tracingEnabled
-
- if {$tracingEnabled} {
- foreach procName $procList {
- ::trace add execution $procName enter \
- [list ::logger::_trace_enter $service]
- ::trace add execution $procName leave \
- [list ::logger::_trace_leave $service]
- }
- }
-}
-
-# This procedure disables Tcl trace handlers for all procedures specified.
-# It is used both to disable Tcl's tracing for a single procedure when
-# removed via "logger::trace remove", as well as to disable all traces
-# via "logger::trace off".
-
-proc ::logger::_disable_traces { service procList } {
- upvar #0 ::logger::tree::${service}::tracingEnabled tracingEnabled
-
- if {$tracingEnabled} {
- foreach procName $procList {
- ::trace remove execution $procName enter \
- [list ::logger::_trace_enter $service]
- ::trace remove execution $procName leave \
- [list ::logger::_trace_leave $service]
- }
- }
-}
-
-########################################################################
-# Trace Handlers
-########################################################################
-
-# This procedure is invoked upon entry into a procedure being traced
-# via "logger::trace add" when tracing is enabled via "logger::trace on"
-# to log information about how the procedure was called.
-
-proc ::logger::_trace_enter { service cmd op } {
- # Parse the command
- set procName [uplevel 1 namespace origin [lindex $cmd 0]]
- set args [lrange $cmd 1 end]
-
- # Display the message prefix
- set callerLvl [expr {[::info level] - 1}]
- set calledLvl [::info level]
-
- lappend message "proc" $procName
- lappend message "level" $calledLvl
- lappend message "script" [uplevel ::info script]
-
- # Display the caller information
- set caller ""
- if {$callerLvl >= 1} {
- # Display the name of the caller proc w/prepended namespace
- catch {
- set callerProcName [lindex [::info level $callerLvl] 0]
- set caller [uplevel 2 namespace origin $callerProcName]
- }
- }
-
- lappend message "caller" $caller
-
- # Display the argument names and values
- set argSpec [uplevel 1 ::info args $procName]
- set argList ""
- if {[llength $argSpec]} {
- foreach argName $argSpec {
- lappend argList $argName
-
- if {$argName == "args"} {
- lappend argList $args
- break
- } else {
- lappend argList [lindex $args 0]
- set args [lrange $args 1 end]
- }
- }
- }
-
- lappend message "procargs" $argList
- set message [list $op $message]
-
- ::logger::tree::${service}::tracecmd $message
-}
-
-# This procedure is invoked upon leaving into a procedure being traced
-# via "logger::trace add" when tracing is enabled via "logger::trace on"
-# to log information about the result of the procedure call.
-
-proc ::logger::_trace_leave { service cmd status rc op } {
- variable RETURN_CODES
-
- # Parse the command
- set procName [uplevel 1 namespace origin [lindex $cmd 0]]
-
- # Gather the caller information
- set callerLvl [expr {[::info level] - 1}]
- set calledLvl [::info level]
-
- lappend message "proc" $procName "level" $calledLvl
- lappend message "script" [uplevel ::info script]
-
- # Get the name of the proc being returned to w/prepended namespace
- set caller ""
- catch {
- set callerProcName [lindex [::info level $callerLvl] 0]
- set caller [uplevel 2 namespace origin $callerProcName]
- }
-
- lappend message "caller" $caller
-
- # Convert the return code from numeric to verbal
-
- if {$status < [llength $RETURN_CODES]} {
- set status [lindex $RETURN_CODES $status]
- }
-
- lappend message "status" $status
- lappend message "result" $rc
-
- # Display the leave message
-
- set message [list $op $message]
- ::logger::tree::${service}::tracecmd $message
-
- return 1
-}
-
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/md5-2.0.8.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/md5-2.0.8.tm
deleted file mode 100644
index 51f35dce..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/md5-2.0.8.tm
+++ /dev/null
@@ -1,739 +0,0 @@
-# md5.tcl - Copyright (C) 2003 Pat Thoyts
-#
-# MD5 defined by RFC 1321, "The MD5 Message-Digest Algorithm"
-# HMAC defined by RFC 2104, "Keyed-Hashing for Message Authentication"
-#
-# This is an implementation of MD5 based upon the example code given in
-# RFC 1321 and upon the tcllib MD4 implementation and taking some ideas
-# from the earlier tcllib md5 version by Don Libes.
-#
-# This implementation permits incremental updating of the hash and
-# provides support for external compiled implementations either using
-# critcl (md5c) or Trf.
-#
-# -------------------------------------------------------------------------
-# See the file "license.terms" for information on usage and redistribution
-# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
-# -------------------------------------------------------------------------
-
-package require Tcl 8.2-; # tcl minimum version
-
-namespace eval ::md5 {
- variable accel
- array set accel {critcl 0 cryptkit 0 trf 0}
-
- namespace export md5 hmac MD5Init MD5Update MD5Final
-
- variable uid
- if {![info exists uid]} {
- set uid 0
- }
-}
-
-# -------------------------------------------------------------------------
-
-# MD5Init --
-#
-# Create and initialize an MD5 state variable. This will be
-# cleaned up when we call MD5Final
-#
-proc ::md5::MD5Init {} {
- variable accel
- variable uid
- set token [namespace current]::[incr uid]
- upvar #0 $token state
-
- # RFC1321:3.3 - Initialize MD5 state structure
- array set state \
- [list \
- A [expr {0x67452301}] \
- B [expr {0xefcdab89}] \
- C [expr {0x98badcfe}] \
- D [expr {0x10325476}] \
- n 0 i "" ]
- if {$accel(cryptkit)} {
- cryptkit::cryptCreateContext state(ckctx) CRYPT_UNUSED CRYPT_ALGO_MD5
- } elseif {$accel(trf)} {
- set s {}
- switch -exact -- $::tcl_platform(platform) {
- windows { set s [open NUL w] }
- unix { set s [open /dev/null w] }
- }
- if {$s != {}} {
- fconfigure $s -translation binary -buffering none
- ::md5 -attach $s -mode write \
- -read-type variable \
- -read-destination [subst $token](trfread) \
- -write-type variable \
- -write-destination [subst $token](trfwrite)
- array set state [list trfread 0 trfwrite 0 trf $s]
- }
- }
- return $token
-}
-
-# MD5Update --
-#
-# This is called to add more data into the hash. You may call this
-# as many times as you require. Note that passing in "ABC" is equivalent
-# to passing these letters in as separate calls -- hence this proc
-# permits hashing of chunked data
-#
-# If we have a C-based implementation available, then we will use
-# it here in preference to the pure-Tcl implementation.
-#
-proc ::md5::MD5Update {token data} {
- variable accel
- upvar #0 $token state
-
- if {$accel(critcl)} {
- if {[info exists state(md5c)]} {
- set state(md5c) [md5c $data $state(md5c)]
- } else {
- set state(md5c) [md5c $data]
- }
- return
- } elseif {[info exists state(ckctx)]} {
- if {[string length $data] > 0} {
- cryptkit::cryptEncrypt $state(ckctx) $data
- }
- return
- } elseif {[info exists state(trf)]} {
- puts -nonewline $state(trf) $data
- return
- }
-
- # Update the state values
- incr state(n) [string length $data]
- append state(i) $data
-
- # Calculate the hash for any complete blocks
- set len [string length $state(i)]
- for {set n 0} {($n + 64) <= $len} {} {
- MD5Hash $token [string range $state(i) $n [incr n 64]]
- }
-
- # Adjust the state for the blocks completed.
- set state(i) [string range $state(i) $n end]
- return
-}
-
-# MD5Final --
-#
-# This procedure is used to close the current hash and returns the
-# hash data. Once this procedure has been called the hash context
-# is freed and cannot be used again.
-#
-# Note that the output is 128 bits represented as binary data.
-#
-proc ::md5::MD5Final {token} {
- upvar #0 $token state
-
- # Check for either of the C-compiled versions.
- if {[info exists state(md5c)]} {
- set r $state(md5c)
- unset state
- return $r
- } elseif {[info exists state(ckctx)]} {
- cryptkit::cryptEncrypt $state(ckctx) ""
- cryptkit::cryptGetAttributeString $state(ckctx) \
- CRYPT_CTXINFO_HASHVALUE r 16
- cryptkit::cryptDestroyContext $state(ckctx)
- # If nothing was hashed, we get no r variable set!
- if {[info exists r]} {
- unset state
- return $r
- }
- } elseif {[info exists state(trf)]} {
- close $state(trf)
- set r $state(trfwrite)
- unset state
- return $r
- }
-
- # RFC1321:3.1 - Padding
- #
- set len [string length $state(i)]
- set pad [expr {56 - ($len % 64)}]
- if {$len % 64 > 56} {
- incr pad 64
- }
- if {$pad == 0} {
- incr pad 64
- }
-
- #puts "P $pad|bits=[expr {8 * $state(n)}]"
-
- append state(i) [binary format a$pad \x80]
-
- # RFC1321:3.2 - Append length in bits as little-endian wide int.
- append state(i) [binary format ii [expr {8 * $state(n)}] 0]
-
- #puts DATA=[Hex $state(i)]([string length $state(i)])
-
- # Calculate the hash for the remaining block.
- set len [string length $state(i)]
- for {set n 0} {($n + 64) <= $len} {} {
- MD5Hash $token [string range $state(i) $n [incr n 64]]
- }
-
- #puts md5-post__________________________________________
- #parray ::${token}
-
- # RFC1321:3.5 - Output
- set r [bytes $state(A)][bytes $state(B)][bytes $state(C)][bytes $state(D)]
- unset state
-
- #puts HASH=[Hex $r]
- return $r
-}
-
-# -------------------------------------------------------------------------
-# HMAC Hashed Message Authentication (RFC 2104)
-#
-# hmac = H(K xor opad, H(K xor ipad, text))
-#
-
-# HMACInit --
-#
-# This is equivalent to the MD5Init procedure except that a key is
-# added into the algorithm
-#
-proc ::md5::HMACInit {K} {
-
- # Key K is adjusted to be 64 bytes long. If K is larger, then use
- # the MD5 digest of K and pad this instead.
- set len [string length $K]
- if {$len > 64} {
- set tok [MD5Init]
- MD5Update $tok $K
- set K [MD5Final $tok]
- set len [string length $K]
- }
- set pad [expr {64 - $len}]
- append K [string repeat \0 $pad]
-
- # Cacluate the padding buffers.
- set Ki {}
- set Ko {}
- binary scan $K i16 Ks
- foreach k $Ks {
- append Ki [binary format i [expr {$k ^ 0x36363636}]]
- append Ko [binary format i [expr {$k ^ 0x5c5c5c5c}]]
- }
-
- set tok [MD5Init]
- MD5Update $tok $Ki; # initialize with the inner pad
-
- # preserve the Ko value for the final stage.
- # FRINK: nocheck
- set [subst $tok](Ko) $Ko
-
- return $tok
-}
-
-# HMACUpdate --
-#
-# Identical to calling MD5Update
-#
-proc ::md5::HMACUpdate {token data} {
- MD5Update $token $data
- return
-}
-
-# HMACFinal --
-#
-# This is equivalent to the MD5Final procedure. The hash context is
-# closed and the binary representation of the hash result is returned.
-#
-proc ::md5::HMACFinal {token} {
- upvar #0 $token state
-
- set tok [MD5Init]; # init the outer hashing function
- MD5Update $tok $state(Ko); # prepare with the outer pad.
- MD5Update $tok [MD5Final $token]; # hash the inner result
- return [MD5Final $tok]
-}
-
-# -------------------------------------------------------------------------
-# Description:
-# This is the core MD5 algorithm. It is a lot like the MD4 algorithm but
-# includes an extra round and a set of constant modifiers throughout.
-#
-# Note:
-# This function body is substituted later on to inline some of the
-# procedures and to make is a bit more comprehensible.
-#
-set ::md5::MD5Hash_body {
- variable $token
- upvar 0 $token state
-
- #puts TR__=[Hex $msg]([string length $msg])
-
- # RFC1321:3.4 - Process Message in 16-Word Blocks
- binary scan $msg i* blocks
- foreach {X0 X1 X2 X3 X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15} $blocks {
- #puts BL
-
- set A $state(A)
- set B $state(B)
- set C $state(C)
- set D $state(D)
-
- # Round 1
- # Let [abcd k s i] denote the operation
- # a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s).
- # Do the following 16 operations.
- # [ABCD 0 7 1] [DABC 1 12 2] [CDAB 2 17 3] [BCDA 3 22 4]
- set A [expr {$B + (($A + [F $B $C $D] + $X0 + $T01) <<< 7)}]
- set D [expr {$A + (($D + [F $A $B $C] + $X1 + $T02) <<< 12)}]
- set C [expr {$D + (($C + [F $D $A $B] + $X2 + $T03) <<< 17)}]
- set B [expr {$C + (($B + [F $C $D $A] + $X3 + $T04) <<< 22)}]
- # [ABCD 4 7 5] [DABC 5 12 6] [CDAB 6 17 7] [BCDA 7 22 8]
- set A [expr {$B + (($A + [F $B $C $D] + $X4 + $T05) <<< 7)}]
- set D [expr {$A + (($D + [F $A $B $C] + $X5 + $T06) <<< 12)}]
- set C [expr {$D + (($C + [F $D $A $B] + $X6 + $T07) <<< 17)}]
- set B [expr {$C + (($B + [F $C $D $A] + $X7 + $T08) <<< 22)}]
- # [ABCD 8 7 9] [DABC 9 12 10] [CDAB 10 17 11] [BCDA 11 22 12]
- set A [expr {$B + (($A + [F $B $C $D] + $X8 + $T09) <<< 7)}]
- set D [expr {$A + (($D + [F $A $B $C] + $X9 + $T10) <<< 12)}]
- set C [expr {$D + (($C + [F $D $A $B] + $X10 + $T11) <<< 17)}]
- set B [expr {$C + (($B + [F $C $D $A] + $X11 + $T12) <<< 22)}]
- # [ABCD 12 7 13] [DABC 13 12 14] [CDAB 14 17 15] [BCDA 15 22 16]
- set A [expr {$B + (($A + [F $B $C $D] + $X12 + $T13) <<< 7)}]
- set D [expr {$A + (($D + [F $A $B $C] + $X13 + $T14) <<< 12)}]
- set C [expr {$D + (($C + [F $D $A $B] + $X14 + $T15) <<< 17)}]
- set B [expr {$C + (($B + [F $C $D $A] + $X15 + $T16) <<< 22)}]
-
- # Round 2.
- # Let [abcd k s i] denote the operation
- # a = b + ((a + G(b,c,d) + X[k] + Ti) <<< s)
- # Do the following 16 operations.
- # [ABCD 1 5 17] [DABC 6 9 18] [CDAB 11 14 19] [BCDA 0 20 20]
- set A [expr {$B + (($A + [G $B $C $D] + $X1 + $T17) <<< 5)}]
- set D [expr {$A + (($D + [G $A $B $C] + $X6 + $T18) <<< 9)}]
- set C [expr {$D + (($C + [G $D $A $B] + $X11 + $T19) <<< 14)}]
- set B [expr {$C + (($B + [G $C $D $A] + $X0 + $T20) <<< 20)}]
- # [ABCD 5 5 21] [DABC 10 9 22] [CDAB 15 14 23] [BCDA 4 20 24]
- set A [expr {$B + (($A + [G $B $C $D] + $X5 + $T21) <<< 5)}]
- set D [expr {$A + (($D + [G $A $B $C] + $X10 + $T22) <<< 9)}]
- set C [expr {$D + (($C + [G $D $A $B] + $X15 + $T23) <<< 14)}]
- set B [expr {$C + (($B + [G $C $D $A] + $X4 + $T24) <<< 20)}]
- # [ABCD 9 5 25] [DABC 14 9 26] [CDAB 3 14 27] [BCDA 8 20 28]
- set A [expr {$B + (($A + [G $B $C $D] + $X9 + $T25) <<< 5)}]
- set D [expr {$A + (($D + [G $A $B $C] + $X14 + $T26) <<< 9)}]
- set C [expr {$D + (($C + [G $D $A $B] + $X3 + $T27) <<< 14)}]
- set B [expr {$C + (($B + [G $C $D $A] + $X8 + $T28) <<< 20)}]
- # [ABCD 13 5 29] [DABC 2 9 30] [CDAB 7 14 31] [BCDA 12 20 32]
- set A [expr {$B + (($A + [G $B $C $D] + $X13 + $T29) <<< 5)}]
- set D [expr {$A + (($D + [G $A $B $C] + $X2 + $T30) <<< 9)}]
- set C [expr {$D + (($C + [G $D $A $B] + $X7 + $T31) <<< 14)}]
- set B [expr {$C + (($B + [G $C $D $A] + $X12 + $T32) <<< 20)}]
-
- # Round 3.
- # Let [abcd k s i] denote the operation
- # a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s)
- # Do the following 16 operations.
- # [ABCD 5 4 33] [DABC 8 11 34] [CDAB 11 16 35] [BCDA 14 23 36]
- set A [expr {$B + (($A + [H $B $C $D] + $X5 + $T33) <<< 4)}]
- set D [expr {$A + (($D + [H $A $B $C] + $X8 + $T34) <<< 11)}]
- set C [expr {$D + (($C + [H $D $A $B] + $X11 + $T35) <<< 16)}]
- set B [expr {$C + (($B + [H $C $D $A] + $X14 + $T36) <<< 23)}]
- # [ABCD 1 4 37] [DABC 4 11 38] [CDAB 7 16 39] [BCDA 10 23 40]
- set A [expr {$B + (($A + [H $B $C $D] + $X1 + $T37) <<< 4)}]
- set D [expr {$A + (($D + [H $A $B $C] + $X4 + $T38) <<< 11)}]
- set C [expr {$D + (($C + [H $D $A $B] + $X7 + $T39) <<< 16)}]
- set B [expr {$C + (($B + [H $C $D $A] + $X10 + $T40) <<< 23)}]
- # [ABCD 13 4 41] [DABC 0 11 42] [CDAB 3 16 43] [BCDA 6 23 44]
- set A [expr {$B + (($A + [H $B $C $D] + $X13 + $T41) <<< 4)}]
- set D [expr {$A + (($D + [H $A $B $C] + $X0 + $T42) <<< 11)}]
- set C [expr {$D + (($C + [H $D $A $B] + $X3 + $T43) <<< 16)}]
- set B [expr {$C + (($B + [H $C $D $A] + $X6 + $T44) <<< 23)}]
- # [ABCD 9 4 45] [DABC 12 11 46] [CDAB 15 16 47] [BCDA 2 23 48]
- set A [expr {$B + (($A + [H $B $C $D] + $X9 + $T45) <<< 4)}]
- set D [expr {$A + (($D + [H $A $B $C] + $X12 + $T46) <<< 11)}]
- set C [expr {$D + (($C + [H $D $A $B] + $X15 + $T47) <<< 16)}]
- set B [expr {$C + (($B + [H $C $D $A] + $X2 + $T48) <<< 23)}]
-
- # Round 4.
- # Let [abcd k s i] denote the operation
- # a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s)
- # Do the following 16 operations.
- # [ABCD 0 6 49] [DABC 7 10 50] [CDAB 14 15 51] [BCDA 5 21 52]
- set A [expr {$B + (($A + [I $B $C $D] + $X0 + $T49) <<< 6)}]
- set D [expr {$A + (($D + [I $A $B $C] + $X7 + $T50) <<< 10)}]
- set C [expr {$D + (($C + [I $D $A $B] + $X14 + $T51) <<< 15)}]
- set B [expr {$C + (($B + [I $C $D $A] + $X5 + $T52) <<< 21)}]
- # [ABCD 12 6 53] [DABC 3 10 54] [CDAB 10 15 55] [BCDA 1 21 56]
- set A [expr {$B + (($A + [I $B $C $D] + $X12 + $T53) <<< 6)}]
- set D [expr {$A + (($D + [I $A $B $C] + $X3 + $T54) <<< 10)}]
- set C [expr {$D + (($C + [I $D $A $B] + $X10 + $T55) <<< 15)}]
- set B [expr {$C + (($B + [I $C $D $A] + $X1 + $T56) <<< 21)}]
- # [ABCD 8 6 57] [DABC 15 10 58] [CDAB 6 15 59] [BCDA 13 21 60]
- set A [expr {$B + (($A + [I $B $C $D] + $X8 + $T57) <<< 6)}]
- set D [expr {$A + (($D + [I $A $B $C] + $X15 + $T58) <<< 10)}]
- set C [expr {$D + (($C + [I $D $A $B] + $X6 + $T59) <<< 15)}]
- set B [expr {$C + (($B + [I $C $D $A] + $X13 + $T60) <<< 21)}]
- # [ABCD 4 6 61] [DABC 11 10 62] [CDAB 2 15 63] [BCDA 9 21 64]
- set A [expr {$B + (($A + [I $B $C $D] + $X4 + $T61) <<< 6)}]
- set D [expr {$A + (($D + [I $A $B $C] + $X11 + $T62) <<< 10)}]
- set C [expr {$D + (($C + [I $D $A $B] + $X2 + $T63) <<< 15)}]
- set B [expr {$C + (($B + [I $C $D $A] + $X9 + $T64) <<< 21)}]
-
- # Then perform the following additions. (That is, increment each
- # of the four registers by the value it had before this block
- # was started.)
- incr state(A) $A
- incr state(B) $B
- incr state(C) $C
- incr state(D) $D
- }
-
- return
-}
-
-proc ::md5::byte {n v} {expr {((0xFF << (8 * $n)) & $v) >> (8 * $n)}}
-proc ::md5::bytes {v} {
- #format %c%c%c%c [byte 0 $v] [byte 1 $v] [byte 2 $v] [byte 3 $v]
- format %c%c%c%c \
- [expr {0xFF & $v}] \
- [expr {(0xFF00 & $v) >> 8}] \
- [expr {(0xFF0000 & $v) >> 16}] \
- [expr {((0xFF000000 & $v) >> 24) & 0xFF}]
-}
-
-# 32bit rotate-left
-proc ::md5::<<< {v n} {
- return [expr {((($v << $n) \
- | (($v >> (32 - $n)) \
- & (0x7FFFFFFF >> (31 - $n))))) \
- & 0xFFFFFFFF}]
-}
-
-# Convert our <<< pseudo-operator into a procedure call.
-regsub -all -line \
- {\[expr {(\$[ABCD]) \+ \(\((.*)\)\s+<<<\s+(\d+)\)}\]} \
- $::md5::MD5Hash_body \
- {[expr {int(\1 + [<<< [expr {\2}] \3])}]} \
- ::md5::MD5Hash_body
-
-# RFC1321:3.4 - function F
-proc ::md5::F {X Y Z} {
- return [expr {($X & $Y) | ((~$X) & $Z)}]
-}
-
-# Inline the F function
-regsub -all -line \
- {\[F (\$[ABCD]) (\$[ABCD]) (\$[ABCD])\]} \
- $::md5::MD5Hash_body \
- {( (\1 \& \2) | ((~\1) \& \3) )} \
- ::md5::MD5Hash_body
-
-# RFC1321:3.4 - function G
-proc ::md5::G {X Y Z} {
- return [expr {(($X & $Z) | ($Y & (~$Z)))}]
-}
-
-# Inline the G function
-regsub -all -line \
- {\[G (\$[ABCD]) (\$[ABCD]) (\$[ABCD])\]} \
- $::md5::MD5Hash_body \
- {(((\1 \& \3) | (\2 \& (~\3))))} \
- ::md5::MD5Hash_body
-
-# RFC1321:3.4 - function H
-proc ::md5::H {X Y Z} {
- return [expr {$X ^ $Y ^ $Z}]
-}
-
-# Inline the H function
-regsub -all -line \
- {\[H (\$[ABCD]) (\$[ABCD]) (\$[ABCD])\]} \
- $::md5::MD5Hash_body \
- {(\1 ^ \2 ^ \3)} \
- ::md5::MD5Hash_body
-
-# RFC1321:3.4 - function I
-proc ::md5::I {X Y Z} {
- return [expr {$Y ^ ($X | (~$Z))}]
-}
-
-# Inline the I function
-regsub -all -line \
- {\[I (\$[ABCD]) (\$[ABCD]) (\$[ABCD])\]} \
- $::md5::MD5Hash_body \
- {(\2 ^ (\1 | (~\3)))} \
- ::md5::MD5Hash_body
-
-
-# RFC 1321:3.4 step 4: inline the set of constant modifiers.
-namespace eval md5 {
- variable tName
- variable tVal
- variable map
- foreach tName {
- T01 T02 T03 T04 T05 T06 T07 T08 T09 T10
- T11 T12 T13 T14 T15 T16 T17 T18 T19 T20
- T21 T22 T23 T24 T25 T26 T27 T28 T29 T30
- T31 T32 T33 T34 T35 T36 T37 T38 T39 T40
- T41 T42 T43 T44 T45 T46 T47 T48 T49 T50
- T51 T52 T53 T54 T55 T56 T57 T58 T59 T60
- T61 T62 T63 T64
- } tVal {
- 0xd76aa478 0xe8c7b756 0x242070db 0xc1bdceee
- 0xf57c0faf 0x4787c62a 0xa8304613 0xfd469501
- 0x698098d8 0x8b44f7af 0xffff5bb1 0x895cd7be
- 0x6b901122 0xfd987193 0xa679438e 0x49b40821
-
- 0xf61e2562 0xc040b340 0x265e5a51 0xe9b6c7aa
- 0xd62f105d 0x2441453 0xd8a1e681 0xe7d3fbc8
- 0x21e1cde6 0xc33707d6 0xf4d50d87 0x455a14ed
- 0xa9e3e905 0xfcefa3f8 0x676f02d9 0x8d2a4c8a
-
- 0xfffa3942 0x8771f681 0x6d9d6122 0xfde5380c
- 0xa4beea44 0x4bdecfa9 0xf6bb4b60 0xbebfbc70
- 0x289b7ec6 0xeaa127fa 0xd4ef3085 0x4881d05
- 0xd9d4d039 0xe6db99e5 0x1fa27cf8 0xc4ac5665
-
- 0xf4292244 0x432aff97 0xab9423a7 0xfc93a039
- 0x655b59c3 0x8f0ccc92 0xffeff47d 0x85845dd1
- 0x6fa87e4f 0xfe2ce6e0 0xa3014314 0x4e0811a1
- 0xf7537e82 0xbd3af235 0x2ad7d2bb 0xeb86d391
- } {
- lappend map \$$tName $tVal
- }
- set ::md5::MD5Hash_body [string map $map $::md5::MD5Hash_body]
- unset map tName tVal
-}
-
-# Define the MD5 hashing procedure with inline functions.
-proc ::md5::MD5Hash {token msg} $::md5::MD5Hash_body
-unset ::md5::MD5Hash_body
-
-# -------------------------------------------------------------------------
-
-if {[package provide Trf] != {}} {
- interp alias {} ::md5::Hex {} ::hex -mode encode --
-} else {
- proc ::md5::Hex {data} {
- binary scan $data H* result
- return [string toupper $result]
- }
-}
-
-# -------------------------------------------------------------------------
-
-# LoadAccelerator --
-#
-# This package can make use of a number of compiled extensions to
-# accelerate the digest computation. This procedure manages the
-# use of these extensions within the package. During normal usage
-# this should not be called, but the test package manipulates the
-# list of enabled accelerators.
-#
-proc ::md5::LoadAccelerator {name} {
- variable accel
- set r 0
- switch -exact -- $name {
- critcl {
- if {![catch {package require tcllibc}]
- || ![catch {package require md5c}]} {
- set r [expr {[info commands ::md5::md5c] != {}}]
- }
- }
- cryptkit {
- if {![catch {package require cryptkit}]} {
- set r [expr {![catch {cryptkit::cryptInit}]}]
- }
- }
- trf {
- if {![catch {package require Trf}]} {
- set r [expr {![catch {::md5 aa} msg]}]
- }
- }
- default {
- return -code error "invalid accelerator package:\
- must be one of [join [array names accel] {, }]"
- }
- }
- set accel($name) $r
-}
-
-# -------------------------------------------------------------------------
-
-# Description:
-# Pop the nth element off a list. Used in options processing.
-#
-proc ::md5::Pop {varname {nth 0}} {
- upvar $varname args
- set r [lindex $args $nth]
- set args [lreplace $args $nth $nth]
- return $r
-}
-
-# -------------------------------------------------------------------------
-
-# fileevent handler for chunked file hashing.
-#
-proc ::md5::Chunk {token channel {chunksize 4096}} {
- upvar #0 $token state
-
- if {[eof $channel]} {
- fileevent $channel readable {}
- set state(reading) 0
- }
-
- MD5Update $token [read $channel $chunksize]
-}
-
-# -------------------------------------------------------------------------
-
-proc ::md5::md5 {args} {
- array set opts {-hex 0 -filename {} -channel {} -chunksize 4096}
- while {[string match -* [set option [lindex $args 0]]]} {
- switch -glob -- $option {
- -hex { set opts(-hex) 1 }
- -file* { set opts(-filename) [Pop args 1] }
- -channel { set opts(-channel) [Pop args 1] }
- -chunksize { set opts(-chunksize) [Pop args 1] }
- default {
- if {[llength $args] == 1} { break }
- if {[string compare $option "--"] == 0} { Pop args; break }
- set err [join [lsort [array names opts]] ", "]
- return -code error "bad option $option:\
- must be one of $err\nlen: [llength $args]"
- }
- }
- Pop args
- }
-
- if {$opts(-filename) != {}} {
- set opts(-channel) [open $opts(-filename) r]
- fconfigure $opts(-channel) -translation binary
- }
-
- if {$opts(-channel) == {}} {
-
- if {[llength $args] != 1} {
- return -code error "wrong # args:\
- should be \"md5 ?-hex? -filename file | string\""
- }
- set tok [MD5Init]
-
- #puts md5_______________________________________________
- #parray ::${tok}
-
- #puts IN=(([lindex $args 0]))
- MD5Update $tok [lindex $args 0]
-
- #puts md5-final_________________________________________
- #parray ::${tok}
-
- set r [MD5Final $tok]
-
- } else {
-
- set tok [MD5Init]
- # FRINK: nocheck
- set [subst $tok](reading) 1
- fileevent $opts(-channel) readable \
- [list [namespace origin Chunk] \
- $tok $opts(-channel) $opts(-chunksize)]
- vwait [subst $tok](reading)
- set r [MD5Final $tok]
-
- # If we opened the channel - we should close it too.
- if {$opts(-filename) != {}} {
- close $opts(-channel)
- }
- }
-
- if {$opts(-hex)} {
- set r [Hex $r]
- }
- return $r
-}
-
-# -------------------------------------------------------------------------
-
-proc ::md5::hmac {args} {
- array set opts {-hex 0 -filename {} -channel {} -chunksize 4096}
- while {[string match -* [set option [lindex $args 0]]]} {
- switch -glob -- $option {
- -key { set opts(-key) [Pop args 1] }
- -hex { set opts(-hex) 1 }
- -file* { set opts(-filename) [Pop args 1] }
- -channel { set opts(-channel) [Pop args 1] }
- -chunksize { set opts(-chunksize) [Pop args 1] }
- default {
- if {[llength $args] == 1} { break }
- if {[string compare $option "--"] == 0} { Pop args; break }
- set err [join [lsort [array names opts]] ", "]
- return -code error "bad option $option:\
- must be one of $err"
- }
- }
- Pop args
- }
-
- if {![info exists opts(-key)]} {
- return -code error "wrong # args:\
- should be \"hmac ?-hex? -key key -filename file | string\""
- }
-
- if {$opts(-filename) != {}} {
- set opts(-channel) [open $opts(-filename) r]
- fconfigure $opts(-channel) -translation binary
- }
-
- if {$opts(-channel) == {}} {
-
- if {[llength $args] != 1} {
- return -code error "wrong # args:\
- should be \"hmac ?-hex? -key key -filename file | string\""
- }
- set tok [HMACInit $opts(-key)]
- HMACUpdate $tok [lindex $args 0]
- set r [HMACFinal $tok]
-
- } else {
-
- set tok [HMACInit $opts(-key)]
- # FRINK: nocheck
- set [subst $tok](reading) 1
- fileevent $opts(-channel) readable \
- [list [namespace origin Chunk] \
- $tok $opts(-channel) $opts(-chunksize)]
- vwait [subst $tok](reading)
- set r [HMACFinal $tok]
-
- # If we opened the channel - we should close it too.
- if {$opts(-filename) != {}} {
- close $opts(-channel)
- }
- }
-
- if {$opts(-hex)} {
- set r [Hex $r]
- }
- return $r
-}
-
-# -------------------------------------------------------------------------
-
-# Try and load a compiled extension to help.
-namespace eval ::md5 {
- variable e
- foreach e {critcl cryptkit trf} { if {[LoadAccelerator $e]} { break } }
- unset e
-}
-
-package provide md5 2.0.8
-
-# -------------------------------------------------------------------------
-# Local Variables:
-# mode: tcl
-# indent-tabs-mode: nil
-# End:
-
-
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/metaface-1.2.5.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/metaface-1.2.5.tm
deleted file mode 100644
index ebcf579e..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/metaface-1.2.5.tm
+++ /dev/null
@@ -1,6411 +0,0 @@
-package require dictutils
-package provide metaface [namespace eval metaface {
- variable version
- set version 1.2.5
-}]
-
-
-
-
-#example datastructure:
-#$_ID_
-#{
-#i
-# {
-# this
-# {
-# {16 ::p::16 item ::>x {}}
-# }
-# role2
-# {
-# {17 ::p::17 item ::>y {}}
-# {18 ::p::18 item ::>z {}}
-# }
-# }
-#context {}
-#}
-
-#$MAP
-#invocantdata {16 ::p::16 item ::>x {}}
-#interfaces {level0
-# {
-# api0 {stack {123 999}}
-# api1 {stack {333}}
-# }
-# level0_default api0
-# level1
-# {
-# }
-# level1_default {}
-# }
-#patterndata {patterndefaultmethod {}}
-
-
-namespace eval ::p::predator {}
-#temporary alternative to ::p::internals namespace.
-# - place predator functions here until ready to replace internals.
-
-
-namespace eval ::p::snap {
- variable id 0 ;#ever-increasing non-reused snapshot-id to identify ::p::snapshot namespaces used to allow overlay-rollbacks.
-}
-
-
-
-
-# not called directly. Retrieved using 'info body ::p::predator::getprop_template'
-#review - why use a proc instead of storing it as a string?
-proc ::p::predator::getprop_template {_ID_ args} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- if {"%varspace%" eq ""} {
- set ns ::p::${OID}
- } else {
- if {[string match "::*" "%varspace%"]} {
- set ns "%varspace%"
- } else {
- set ns ::p::${OID}::%varspace%
- }
- }
-
-
- if {[llength $args]} {
- #lassign [lindex $invocant 0] OID alias itemCmd cmd
- if {[array exists ${ns}::o_%prop%]} {
- #return [set ${ns}::o_%prop%($args)]
- if {[llength $args] == 1} {
- return [set ::p::${OID}::o_%prop%([lindex $args 0])]
- } else {
- return [lindex [set ::p::${OID}::o_%prop%([lindex $args 0])] {*}[lrange $args 1 end]]
- }
- } else {
- set val [set ${ns}::o_%prop%]
-
- set rType [expr {[scan [namespace tail $val] >%s rType] ? {object} : {unknown}}]
- if {$rType eq "object"} {
- #return [$val . item {*}$args]
- return [$val {*}$args]
- } else {
- #treat as list?
- return [lindex $val $args]
- }
- }
- } else {
- return [set ${ns}::o_%prop%]
- }
-}
-
-
-proc ::p::predator::getprop_template_immediate {_ID_ args} {
- if {[llength $args]} {
- if {[array exists %ns%::o_%prop%]} {
- return [set %ns%::o_%prop%($args)]
- } else {
- set val [set %ns%::o_%prop%]
- set rType [expr {[scan [namespace tail $val] >%s rType] ? {object} : {unknown}}]
- if {$rType eq "object"} {
- #return [$val . item {*}$args]
- #don't assume defaultmethod named 'item'!
- return [$val {*}$args]
- } else {
- #treat as list?
- return [lindex $val $args]
- }
- }
- } else {
- return [set %ns%::o_%prop%]
- }
-}
-
-
-
-
-
-
-
-
-proc ::p::predator::getprop_array {_ID_ prop args} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
-
- #upvar 0 ::p::${OID}::o_${prop} prop
- #1st try: assume array
- if {[catch {array get ::p::${OID}::o_${prop}} result]} {
- #treat as list (why?)
- #!review
- if {[info exists ::p::${OID}::o_${prop}]} {
- array set temp [::list]
- set i 0
- foreach element ::p::${OID}::o_${prop} {
- set temp($i) $element
- incr i
- }
- set result [array get temp]
- } else {
- error "unable to retrieve [set ::p::${OID}::o_${prop}] contents in 'array get' format"
- }
- }
- return $result
-}
-
-proc ::p::predator::setprop_template {prop _ID_ args} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- if {"%varspace%" eq ""} {
- set ns ::p::${OID}
- } else {
- if {[string match "::*" "%varspace%"]} {
- set ns "%varspace%"
- } else {
- set ns ::p::${OID}::%varspace%
- }
- }
-
-
- if {[llength $args] == 1} {
- #return [set ::p::${OID}::o_%prop% [lindex $args 0]]
- return [set ${ns}::o_%prop% [lindex $args 0]]
-
- } else {
- if {[array exists ${ns}::o_%prop%] || ![info exists ${ns}::o_%prop%]} {
- #treat attempt to perform indexed write to nonexistant var, same as indexed write to array
-
- #2 args - single index followed by a value
- if {[llength $args] == 2} {
- return [set ${ns}::o_%prop%([lindex $args 0]) [lindex $args 1]]
- } else {
- #multiple indices
- #return [set ::p::${OID}::o_%prop%([lrange $args 0 end-1]) [lindex $args end]]
- return [lset ${ns}::o_%prop%([lindex $args 0]) {*}[lrange $args 1 end-1] [lindex $args end] ]
- }
- } else {
- #treat as list
- return [lset ${ns}::o_%prop% [lrange $args 0 end-1] [lindex $args end]]
- }
- }
-}
-
-#--------------------------------------
-#property read & write traces
-#--------------------------------------
-
-
-proc ::p::predator::propref_trace_read {get_cmd _ID_ refname prop indices vtraced idx op} {
-
- #puts stderr "\t-->propref_trace_read get_cmd:'$get_cmd' refname:'$refname' prop:'$prop' indices:'$indices' $vtraced idx:'$idx' "
-
- #set cmd ::p::${OID}::(GET)$prop ;#this is an interp alias to the head of the implementation command-chain.
-
- if {[llength $idx]} {
- if {[llength $idx] == 1} {
- set ${refname}($idx) [$get_cmd $_ID_ {*}$indices $idx]
- } else {
- lset ${refname}([lindex $idx 0]) [lrange $idx 1 end] [$get_cmd $_ID_ {*}$indices {*}$idx]
- }
- return ;#return value ignored - in a trace we can only return the value by setting the traced variable to a value
- } else {
- if {![info exists $refname]} {
- set $refname [$get_cmd $_ID_ {*}$indices]
- } else {
- set newval [$get_cmd $_ID_ {*}$indices]
- if {[set $refname] ne $newval} {
- set $refname $newval
- }
- }
- return
- }
-}
-
-
-
-
-proc ::p::predator::propref_trace_write {_ID_ OID full_varspace refname vname idx op} {
- #note 'vname' may be upvar-ed local - we need the fully qualified name so must use passed in $refname
- #puts stdout "\t-->propref_trace_write $OID ref:'$refname' var:'$vname' idx:'$idx'"
-
-
- #derive the name of the write command from the ref var.
- set indices [lassign [split [namespace tail $refname] +] prop]
-
-
- #assert - we will never have both a list in indices and an idx value
- if {[llength $indices] && ($idx ne "")} {
- #since Tcl has no nested arrays - we can't write to an idx within something like ${prop}+x
- #review - are there any datastructures which would/should allow this?
- #this assertion is really just here as a sanity check for now
- error "propref_trace_write unexpected values. Didn't expect a refname of the form ${prop}+* as well as an idx value"
- }
-
- #upvar #0 ::p::${OID}::_meta::map MAP
- #puts "-->propref_trace_write map: $MAP"
-
- #temporarily deactivate refsync trace
- #puts stderr -->1>--removing_trace_o_${field}
-### trace remove variable ::p::${OID}::o_${prop} [::list write] [::list ::p::predator::propvar_write_TraceHandler $OID $prop]
-
- #we need to catch, and re-raise any error that we may receive when writing the property
- # because we have to reinstate the propvar_write_TraceHandler after the call.
- #(e.g there may be a propertywrite handler that deliberately raises an error)
-
- set excludesync_refs $refname
- set cmd ::p::${OID}::(SET)$prop
-
-
- set f_error 0
- if {[catch {
-
- if {![llength $indices]} {
- if {[string length $idx]} {
- $cmd $_ID_ $idx [set ${refname}($idx)]
- #::p::predator::refsyncvar_write_manualupdate $OID $excludesync_refs $prop ::p::${OID}::o_${prop}($idx) [list]
- ### ::p::predator::refsyncvar_write_manualupdate $OID $excludesync_refs $prop ::p::${OID}::o_${prop} [list $idx]
-
- } else {
- $cmd $_ID_ [set $refname]
- ### ::p::predator::refsyncvar_write_manualupdate $OID $excludesync_refs $prop ::p::${OID}::o_${prop} [list]
- }
- } else {
- #puts " ++>> cmd:$cmd indices:'$indices' refname:'$refname'\n"
- $cmd $_ID_ {*}$indices [set $refname]
- ### ::p::predator::refsyncvar_write_manualupdate $OID $excludesync_refs $prop ::p::${OID}::o_${prop} $indices
- }
-
- } result]} {
- set f_error 1
- }
-
-
-
-
- #::p::predator::propvar_write_TraceHandler $OID $prop ::p::${OID}::o_${prop} $indices write
- #reactivate refsync trace
- #puts stderr "****** reactivating refsync trace on o_$field"
- #puts stderr -->2>--reactivating_trace_o_${field}
- ### trace add variable ::p::${OID}::o_${prop} [::list write] [::list ::p::predator::propvar_write_TraceHandler $OID $prop]
-
-
- if {$f_error} {
- #!todo - review error & 'return' functions for proper way to throw error, preserving callstack info for debugging.
- # ? return -code error $errMsg ? -errorinfo
-
- #!quick n dirty
- #error $errorMsg
- return -code error -errorinfo $::errorInfo $result
- } else {
- return $result
- }
-}
-
-
-
-
-
-proc ::p::predator::propref_trace_array {_ID_ OID refname vref idx op} {
- #puts stderr "\t-->propref_trace_array OID:$OID refname:'$refname' var:'$vref' index:'$idx' operation:'$op'"
- #NOTE - do not rely on $vref !!!! (can be upvared - so could be anything. e.g during 'parray' calls it is set to 'array')
-
- set indices [lassign [split [namespace tail $refname] +] prop] ;#make sure 'prop' is set
-
- #set updated_value [::p::predator::getprop_array $prop $_ID_]
- #puts stderr "-->array_Trace updated_value:$updated_value"
- if {[catch {array set $refname [::p::predator::getprop_array $_ID_ $prop ]} errm]} {
- puts stderr "-->propref_trace_array error $errm"
- array set $refname {}
- }
-
- #return value ignored for
-}
-
-
-#--------------------------------------
-#
-proc ::p::predator::object_array_trace {OID _ID_ vref idx op} {
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd
-
-
- #don't rely on variable name passed by trace - may have been 'upvar'ed
- set refvar ::p::${OID}::_ref::__OBJECT
-
- #puts "+=====>object_array_trace $map '$vref' '$idx' '$op' refvar: $refvar"
-
- set iflist [dict get $MAP interfaces level0]
-
- set plist [list]
-
- #!todo - get propertylist from cache on object(?)
- foreach IFID [lreverse $iflist] {
- dict for {prop pdef} [set ::p::${IFID}::_iface::o_properties] {
- #lassign $pdef v
- if {[catch {lappend plist $prop [set ::p::${OID}::o_${prop}]}]} {
- if {[array exists ::p::${OID}::o_${prop}]} {
- lappend plist $prop [array get ::p::${OID}::o_${prop}]
- } else {
- #ignore - array only represents properties that have been set.
- #error "property $v is not set"
- #!todo - unset corresponding items in $refvar if needed?
- }
- }
- }
- }
- array set $refvar $plist
-}
-
-
-proc ::p::predator::object_read_trace {OID _ID_ vref idx op} {
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd
- #don't rely on variable name passed by trace.
- set refvar ::p::${OID}::_ref::__OBJECT
-
- #puts "\n\n+=====>object_read_trace map:'$MAP' '$vref' '$idx' '$op' refvar: $refvar\n\n"
-
- #!todo? - build a list of all interface properties (cache it on object??)
- set iflist [dict get $MAP interfaces level0]
- set IID ""
- foreach id [lreverse $iflist] {
- if {$idx in [dict keys [set ::p::${id}::_iface::o_properties]]} {
- set IID $id
- break
- }
- }
-
- if {[string length $IID]} {
- #property
- if {[catch {set ${refvar}($idx) [::p::${id}::_iface::(GET)$idx $_ID_]} errmsg]} {
- puts stderr "\twarning: ::p::${id}::_iface::(GET)$idx retrieval failed (array?) errmsg:$errmsg"
- }
- } else {
- #method
- error "property '$idx' not found"
- }
-}
-
-
-proc ::p::predator::object_unset_trace {OID _ID_ vref idx op} {
- upvar #0 ::p::${OID}::_meta::map MAP
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd
-
- #!todo - ???
-
- if {![llength [info commands ::p::${OID}::$idx]]} {
- error "no such method or property: '$idx'"
- } else {
- #!todo? - build a list of all interface properties (cache it on object??)
- set iflist [dict get $MAP interfaces level0]
- set found 0
- foreach id [lreverse $iflist] {
- if {$idx in [dict keys [set ::p::${id}::_iface::o_properties]]} {
- set found 1
- break
- }
- }
-
- if {$found} {
- unset ::p::${OID}::o_$idx
- } else {
- puts stderr "\tWARNING: UNIMPLEMENTED CASE! (unset) object_unset_trace id:$OID objectcmd:[lindex [dict get $MAP invocantdata] 3] var:$vref prop:$idx"
- }
- }
-}
-
-
-proc ::p::predator::object_write_trace {OID _ID_ vref idx op} {
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd
- #don't rely on variable name passed by trace.
- set refvar ::p::${OID}::_ref::__OBJECT
- #puts "+=====>object_write_trace $MAP '$vref' '$idx' '$op' refvar: $refvar"
-
-
- if {![llength [info commands ::p::${OID}::$idx]]} {
- #!todo - create new property in interface upon attempt to write to non-existant?
- # - or should we require some different kind of object-reference for that?
- array unset $refvar $idx ;#make sure 'array names' on the ref doesn't include this $idx
- error "no such method or property: '$idx'"
- } else {
- #!todo? - build a list of all interface properties (cache it on object??)
- set iflist [dict get $MAP interfaces level0]
- set IID ""
- foreach id [lreverse $iflist] {
- if {$idx in [dict keys [set ::p::${id}::_iface::o_properties]]} {
- set IID $id
- break
- }
- }
-
- #$IID is now topmost interface in default iStack which has this property
-
- if {[string length $IID]} {
- #write to defined property
-
- ::p::${IID}::_iface::(SET)$idx $_ID_ [set ${refvar}($idx)]
- } else {
- #!todo - allow write of method body back to underlying object?
- #attempted write to 'method' ..undo(?)
- array unset $refvar $idx ;#make sure 'array names' on the ref doesn't include this $idx
- error "cannot write to method '$idx'"
- #for now - disallow
- }
- }
-
-}
-
-
-
-proc ::p::predator::propref_trace_unset {_ID_ OID refname vref idx op} {
- #note 'vref' may be upvar-ed local - we need the fully qualified name so must use passed in $refname
-
- set refindices [lassign [split [namespace tail $refname] +] prop]
- #derive the name of any potential PropertyUnset command from the refname. i.e (UNSET)$prop
- #if there is no PropertyUnset command - we unset the underlying variable directly
-
- trace remove variable ::p::${OID}::o_${prop} [::list unset] [::list ::p::predator::propvar_unset_TraceHandler $OID $prop]
-
-
- if {[catch {
-
- #assert if refname is complex (prop+idx etc), we will not get a reference trace with an $idx value
- #i.e
- if {[llength $refindices] && [string length $idx]} {
- puts stderr "\t !!!!! unexpected call to propref_trace_unset oid:'$OID' refname:'$refname' vref:'$vref' idx:'$idx' op:'$op'"
- error "unexpected call to propref_trace_unset"
- }
-
-
- upvar #0 ::p::${OID}::_meta::map MAP
-
- set iflist [dict get $MAP interfaces level0]
- #find topmost interface containing this $prop
- set IID ""
- foreach id [lreverse $iflist] {
- if {$prop in [dict keys [set ::p::${id}::_iface::o_properties]]} {
- set IID $id
- break
- }
- }
- if {![string length $IID]} {
- error "propref_trace_unset failed to find property '$prop' on objectid $OID ([lindex [dict get $_ID_ i this] 0 3])"
- }
-
-
-
-
-
-
- if {[string length $idx]} {
- #eval "$_alias ${unset_}$field $idx"
- #what happens to $refindices???
-
-
- #!todo varspace
-
- if {![llength $refindices]} {
- #puts stdout "\t 1a@@@@ propref_trace_unset $OID ref:'$refname' var:'$vref' idx:'$idx'"
-
- if {![llength [info commands ::p::${IID}::_iface::(UNSET)$prop]]} {
- unset ::p::${OID}::o_${prop}($idx)
- } else {
- ::p::${IID}::_iface::(UNSET)$prop $_ID_ $idx
- }
-
-
- #manually call refsync, passing it this refvar as an exclusion
- ::p::predator::refsyncvar_unset_manualupdate $OID $refname $prop ::p::${OID}::o_${prop} $idx
- } else {
- #assert - won't get here
- error 1a
-
- }
-
- } else {
- if {[llength $refindices]} {
- #error 2a
- #puts stdout "\t 2a@@@@ propref_trace_unset $OID ref:'$refname' var:'$vref' idx:'$idx'"
-
- if {![llength [info commands ::p::${IID}::_iface::(UNSET)$prop]]} {
- #review - what about list-type property?
- #if {[array exists ::p::${OID}::o_${prop}]} ???
- unset ::p::${OID}::o_${prop}($refindices)
- } else {
- ::p::${IID}::_iface::(UNSET)$prop $_ID_ $refindices
- }
-
-
-
- #manually call refsync, passing it this refvar as an exclusion
- ::p::predator::refsyncvar_unset_manualupdate $OID $refname $prop ::p::${OID}::o_${prop} $refindices
-
-
- } else {
- #puts stdout "\t 2b@@@@ propref_trace_unset $OID ref:'$refname' var:'$vref' idx:'$idx'"
-
- #ref is not of form prop+x etc and no idx in the trace - this is a plain unset
- if {![llength [info commands ::p::${IID}::_iface::(UNSET)$prop]]} {
- unset ::p::${OID}::o_${prop}
- } else {
- ::p::${IID}::_iface::(UNSET)$prop $_ID_ ""
- }
- #manually call refsync, passing it this refvar as an exclusion
- ::p::predator::refsyncvar_unset_manualupdate $OID $refname $prop ::p::${OID}::o_${prop} {}
-
- }
- }
-
-
-
-
- } errM]} {
- #set ::LAST_UNSET_ERROR "$errM\n[set ::errorInfo]"
- set ruler [string repeat - 80]
- puts stderr "\t$ruler"
- puts stdout "\t @@@@ERROR propref_trace_unset $OID ref:'$refname' var:'$vref' idx:'$idx'"
- puts stderr "\t$ruler"
- puts stderr $errM
- puts stderr "\t$ruler"
-
- } else {
- #puts stdout "\t @@@@ propref_trace_unset $OID ref:'$refname' var:'$vref' idx:'$idx'"
- #puts stderr "*@*@*@*@ end propref_trace_unset - no error"
- }
-
- trace add variable ::p::${OID}::o_${prop} [::list unset] [::list ::p::predator::propvar_unset_TraceHandler $OID $prop]
-
-
-}
-
-
-
-
-proc ::p::predator::refsyncvar_unset_manualupdate {OID triggeringRef prop vtraced vidx} {
-
- #Do not use 'info exists' (avoid triggering read trace) - use info vars
- if {[info vars ::p::${OID}::_ref::$prop] eq "::p::${OID}::_ref::$prop"} {
- #puts " **> lappending '::p::REF::${OID}::$prop'"
- lappend refvars ::p::${OID}::_ref::$prop
- }
- lappend refvars {*}[info vars ::p::${OID}::_ref::${prop}+*]
-
-
-
- if {[string length $triggeringRef]} {
- set idx [lsearch -exact $refvars $triggeringRef]
- if {$idx >= 0} {
- set refvars [lreplace $refvars[set refvars {}] $idx $idx] ;#note inline K combinator [set refvars {}]
- }
- }
- if {![llength $refvars]} {
- #puts stderr " %%%%%%%%%% no refvars for propvar_unset_TraceHandler to update - short circuiting . $OID $triggeringRef $prop $vtraced $vidx"
- return
- }
-
-
- #*usually* triggeringRef is not in the reflist because the triggeringRef is being unset
- # - but this is not the case when we do an array unset of an element using a reference to the whole array e.g "array unset [>obj . arr .] b"
- if {([string length $triggeringRef]) && ($triggeringRef in $refvars)} {
- #puts stderr "\t@@@@@@@@@@ propvar_unset_TraceHandler unexpected situation. triggeringRef $triggeringRef in refvars:$refvars during unset ???"
- puts stderr "\t@@@@@ propvar_unset_TraceHandler triggeringRef $triggeringRef is in refvars list - probably a call of form 'array unset \[>obj .arr .\] someindex'"
- }
-
-
- puts stderr "\t refsyncvar_unset_manualupdate OID:'$OID' triggeringRef:'$triggeringRef' prop:'$prop' vtraced:'$vtraced' vidx:'$vidx' "
-
-
-
- upvar $vtraced SYNCVARIABLE
-
-
- #We are only interested in suppressing the 'setGet_TraceHandler' traces on refvars
- array set traces [::list]
-
- #puts stderr "*-*-*-*-*-* refvars \n- [join $refvars "\n- "]"
-
-
- foreach rv $refvars {
- #puts "--refvar $rv"
- foreach tinfo [trace info variable $rv] {
- #puts "##trace $tinfo"
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
- #!warning - assumes traces with single operation per handler.
- #write & unset traces on refvars need to be suppressed
- #we also need to be able to read certain refvars without triggering retrieval of underlying value in order to detect if changed.
- if {$ops in {read write unset array}} {
- if {[string match "::p::predator::propref_trace_*" $cmd]} {
- lappend traces($rv) $tinfo
- trace remove variable $rv $ops $cmd
- #puts stderr "*-*-*-*-*-* removing $ops trace on $rv -> $cmd"
- }
- }
- }
- }
-
-
-
-
- if {[array exists SYNCVARIABLE]} {
-
- #underlying variable is an array - we are presumably unsetting just an element
- set vtracedIsArray 1
- } else {
- #!? maybe the var was an array - but it's been unset?
- set vtracedIsArray 0
- }
-
- #puts stderr "--------------------------------------------------\n\n"
- #some things we don't want to repeat for each refvar in case there are lots of them..
-
- #set triggeringRefIdx $vidx
-
- if {[string match "${prop}+*" [namespace tail $triggeringRef]]} {
- set triggering_indices [lrange [split [namespace tail $triggeringRef] +] 1 end]
- } else {
- set triggering_indices [list]
- }
-
-
-
-
- #puts stderr ">>>...----refsync-trace = $vtraced $op refvars:$refvars"
- #puts stderr ">>> [trace info variable $vtraced]"
- #puts "--- unset branch refvar:$refvar"
-
-
-
- if {[llength $vidx]} {
- #trace called with an index - must be an array
- foreach refvar $refvars {
- set reftail [namespace tail $refvar]
-
- if {[string match "${prop}+*" $reftail]} {
- #!todo - add test
- if {$vidx eq [lrange [split $reftail +] 1 end]} {
- #unset if indices match
- error "untested, possibly unused branch spuds1"
- #puts "1111111111111111111111111"
- unset $refvar
- }
- } else {
- #test exists - #!todo - document which one
-
- #see if we succeeded in unsetting this element in the underlying variables
- #(may have been blocked by a PropertyUnset body)
- set element_exists [uplevel 1 [::list info exists ${vtraced}($vidx)]]
- #puts "JJJJJJ vtraced:$vtraced vidx:$vidx element_exists:$element_exists"
- if {$element_exists} {
- #do nothing it wasn't actually unset
- } else {
- #puts "JJJJJ unsetting ${refvar}($vidx)"
- unset ${refvar}($vidx)
- }
- }
- }
-
-
-
-
-
- } else {
-
- foreach refvar $refvars {
- set reftail [namespace tail $refvar]
-
- if {[string match "${prop}+*" $reftail]} {
- #check indices of triggering refvar match this refvars indices
-
-
- if {$reftail eq [namespace tail $triggeringRef]} {
- #!todo - add test
- error "untested, possibly unused branch spuds2"
- #puts "222222222222222222"
- unset $refvar
- } else {
-
- #error "untested - branch spuds2a"
-
-
- }
-
- } else {
- #!todo -add test
- #reference is directly to property var
- error "untested, possibly unused branch spuds3"
- #theoretically no other non-indexed ref.. so $triggeringRefIdx must contain non-zero-len string?
- puts "\t33333333333333333333"
-
- if {[string length $triggeringRefIdx]} {
- unset $refvar($triggeringRefIdx)
- }
- }
- }
-
- }
-
-
-
-
- #!todo - understand.
- #puts stderr "\n*****\n propvar_unset_TraceHandler $refvar unset $prop $args \n*****\n"
- #catch {unset $refvar} ;#oops - Tcl_EventuallyFree called twice - abnormal program termination (tcl8.4?)
-
-
- #reinstall the traces we stored at the beginning of this proc.
- foreach rv [array names traces] {
- foreach tinfo $traces($rv) {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
-
- #puts stderr "****** re-installing setGet trace '$ops' on variable $rv"
- trace add variable $rv $ops $cmd
- }
- }
-
-
-
-
-
-}
-
-
-proc ::p::predator::propvar_unset_TraceHandler {OID prop vtraced vidx op} {
-
- upvar $vtraced SYNCVARIABLE
-
- set refvars [::list]
- #Do not use 'info exists' (avoid triggering read trace) - use info vars
- if {[info vars ::p::${OID}::_ref::$prop] eq "::p::${OID}::_ref::$prop"} {
- lappend refvars ::p::${OID}::_ref::$prop
- }
- lappend refvars {*}[info vars ::p::${OID}::_ref::${prop}+*]
-
-
-
- #short_circuit breaks unset traces for array elements (why?)
-
-
- if {![llength $refvars]} {
- #puts stderr "\t%%%%%%%%%% no refvars for propvar_unset_TraceHandler to update - short circuiting . OID:'$OID' prop:'$prop' vtraced:'$vtraced' vidx:'$vidx'"
- return
- } else {
- puts stderr "\t****** [llength $refvars] refvars for propvar_unset_TraceHandler to update. OID:'$OID' prop:'$prop' vtraced:'$vtraced' vidx:'$vidx'"
- }
-
- if {[catch {
-
-
-
- #We are only interested in suppressing the 'setGet_TraceHandler' traces on refvars
- array set traces [::list]
-
- #puts stderr "*-*-*-*-*-* refvars \n- [join $refvars "\n- "]"
-
-
- foreach rv $refvars {
- #puts "--refvar $rv"
- foreach tinfo [trace info variable $rv] {
- #puts "##trace $tinfo"
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
- #!warning - assumes traces with single operation per handler.
- #write & unset traces on refvars need to be suppressed
- #we also need to be able to read certain refvars without triggering retrieval of underlying value in order to detect if changed.
- if {$ops in {read write unset array}} {
- if {[string match "::p::predator::propref_trace_*" $cmd]} {
- lappend traces($rv) $tinfo
- trace remove variable $rv $ops $cmd
- #puts stderr "*-*-*-*-*-* removing $ops trace on $rv -> $cmd"
- }
- }
- }
- }
-
-
-
-
- if {[array exists SYNCVARIABLE]} {
-
- #underlying variable is an array - we are presumably unsetting just an element
- set vtracedIsArray 1
- } else {
- #!? maybe the var was an array - but it's been unset?
- set vtracedIsArray 0
- }
-
- #puts stderr "--------------------------------------------------\n\n"
- #some things we don't want to repeat for each refvar in case there are lots of them..
- set triggeringRefIdx $vidx
-
-
-
- #puts stderr ">>>...----refsync-trace = $vtraced $op refvars:$refvars"
- #puts stderr ">>> [trace info variable $vtraced]"
- #puts "--- unset branch refvar:$refvar"
-
-
-
- if {[llength $vidx]} {
- #trace called with an index - must be an array
- foreach refvar $refvars {
- set reftail [namespace tail $refvar]
-
- if {[string match "${prop}+*" $reftail]} {
- #!todo - add test
- if {$vidx eq [lrange [split $reftail +] 1 end]} {
- #unset if indices match
- error "untested, possibly unused branch spuds1"
- #puts "1111111111111111111111111"
- unset $refvar
- }
- } else {
- #test exists - #!todo - document which one
-
- #see if we succeeded in unsetting this element in the underlying variables
- #(may have been blocked by a PropertyUnset body)
- set element_exists [uplevel 1 [::list info exists ${vtraced}($vidx)]]
- #puts "JJJJJJ vtraced:$vtraced vidx:$vidx element_exists:$element_exists"
- if {$element_exists} {
- #do nothing it wasn't actually unset
- } else {
- #puts "JJJJJ unsetting ${refvar}($vidx)"
- unset ${refvar}($vidx)
- }
- }
- }
-
-
-
-
-
- } else {
-
- foreach refvar $refvars {
- set reftail [namespace tail $refvar]
- unset $refvar
-
- }
-
- }
-
-
-
-
- #!todo - understand.
- #puts stderr "\n*****\n propvar_unset_TraceHandler $refvar unset $prop $args \n*****\n"
- #catch {unset $refvar} ;#oops - Tcl_EventuallyFree called twice - abnormal program termination (tcl8.4?)
-
-
- #reinstall the traces we stored at the beginning of this proc.
- foreach rv [array names traces] {
- foreach tinfo $traces($rv) {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
-
- #puts stderr "****** re-installing setGet trace '$ops' on variable $rv"
- trace add variable $rv $ops $cmd
- }
- }
-
- } errM]} {
- set ruler [string repeat * 80]
- puts stderr "\t$ruler"
- puts stderr "\t>>>>>>>$ propvar_unset_TraceHandler OID:'$OID' prop:'$prop' vtraced:'$vtraced' vidx:'$vidx' $op"
- puts stderr "\t$ruler"
- puts stderr $::errorInfo
- puts stderr "\t$ruler"
-
- }
-
-}
-
-proc ::p::predator::refsyncvar_write_manualupdate {OID triggeringRef prop vtraced indices} {
- error hmmmmm
- upvar $vtraced SYNCVARIABLE
- #puts stderr "\t>>>>>>>$ refsyncvar_write_manualupdate $OID '$triggeringRef' '$prop' vtraced:'$vtraced' indices:'$indices' "
- set refvars [::list]
-
- #avoid info exists ::p::${OID}::_ref::$prop (info exists triggers read unnecessary read trace )
- if {[info vars ::p::${OID}::_ref::$prop] eq "::p::${OID}::_ref::$prop"} {
- lappend refvars ::p::${OID}::_ref::$prop ;#this is the basic unindexed reference we normally get when getting a standard property ref (e.g set ref [>obj . prop .])
- }
- lappend refvars {*}[info vars ::p::${OID}::_ref::${prop}+*] ;#add any indexed references
- #assert triggeringRef is in the list
- if {([string length $triggeringRef]) && ($triggeringRef ni $refvars)} {
- error "@@@@@@@@@@ refsyncvar_write_manualupdate unexpected situation. triggeringRef $triggeringRef ni refvars:$refvars"
- }
- set refposn [lsearch -exact $refvars $triggeringRef]
- #assert - due to test above, we know $triggeringRef is in the list so refposn > 0
- set refvars [lreplace $refvars[set refvars {}] $refposn $refposn] ;#note inline K combinator [set refvars {}]
- if {![llength $refvars]} {
- #puts stderr " %%%%%%%%%% no refvars for refsyncvar_write_manualupdate to update - short circuiting . OID:$OID prop:$prop"
- return [list refs_updates [list]]
- }
-
- #suppress the propref_trace_* traces on all refvars
- array set traces [::list]
- array set external_traces [::list] ;#e.g application/3rd party traces on "">obj . prop ."
- #we do not support tracing of modifications to refs which occur from inside the pattern system. ie we disable them during refsync
- #todo - after finished refsyncing - consider manually firing the external_traces in such a way that writes/unsets raise an error?
- #(since an external trace should not be able to affect a change which occured from inside the object - but can affect values from application writes/unsets to the ref)
-
- foreach rv $refvars {
- #puts "--refvar $rv"
- foreach tinfo [trace info variable $rv] {
- #puts "##trace $tinfo"
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
- #!warning - assumes traces with single operation per handler.
- #write & unset traces on refvars need to be suppressed
- #we also need to be able to read certain refvars without triggering retrieval of underlying value in order to detect if changed.
-
-
- if {[string match "::p::predator::propref_trace_*" $cmd]} {
- lappend traces($rv) $tinfo
- trace remove variable $rv $ops $cmd
- #puts stderr "*-*-*-*-*-* removing $ops trace on $rv -> $cmd"
- } else {
- #all other traces are 'external'
- lappend external_traces($rv) $tinfo
- #trace remove variable $rv $ops $cmd
- }
-
- }
- }
- #--------------------------------------------------------------------------------------------------------------------------
- if {([array exists SYNCVARIABLE]) || (![info exists SYNCVARIABLE])} {
- if {![info exists SYNCVARIABLE]} {
- error "WARNING: REVIEW why does $vartraced not exist here?"
- }
- #either the underlying variable is an array
- # OR - underlying variable doesn't exist - so we treat the property as an array because of the indexed access pattern
- set treat_vtraced_as_array 1
- } else {
- set treat_vtraced_as_array 0
- }
-
- set refs_updated [list]
- set refs_deleted [list] ;#unset due to index no longer being relevant
- if {$treat_vtraced_as_array} {
- foreach refvar $refvars {
- #puts stdout "\n\n \tarrayvariable:'$vtraced' examining REFVAR:'$refvar'"
- set refvar_tail [namespace tail $refvar]
- if {[string match "${prop}+*" $refvar_tail]} {
- #refvar to update is curried e.g ::p::${OID}::_ref::${prop}+x+y
- set ref_indices [lrange [split $refvar_tail +] 1 end]
- if {[llength $indices]} {
- if {[llength $indices] == 1} {
- if {[lindex $ref_indices 0] eq [lindex $indices 0]} {
- #error "untested xxx-a"
- set ${refvar} [set SYNCVARIABLE([lindex $indices 0])]
- lappend refs_updated $refvar
- } else {
- #test exists
- #error "xxx-ok single index"
- #updating a different part of the property - nothing to do
- }
- } else {
- #nested index
- if {[lindex $ref_indices 0] eq [lindex $indices 0]} {
- if {[llength $ref_indices] == 1} {
- #error "untested xxx-b1"
- set ${refvar} [lindex [set SYNCVARIABLE([lindex $indices 0])] [lrange $indices 1 end] ]
- } else {
- #assert llength $ref_indices > 1
- #NOTE - we cannot test index equivalence reliably/simply just by comparing indices
- #compare by value
-
- if {![catch {lindex [set SYNCVARIABLE([lindex $indices 0])] [lrange $indices 1 end]} possiblyNewVal]} {
- #puts stderr "\tYYYYYYYYY $refvar:'[set $refvar]'' / possiblyNewVal:'$possiblyNewVal'"
- if {[set $refvar] ne $possiblyNewVal} {
- set $refvar $possiblyNewVal
- }
- } else {
- #fail to retrieve underlying value corrsponding to these $indices
- unset $refvar
- }
- }
- } else {
- #test exists
- #error "untested xxx-ok deepindex"
- #updating a different part of the property - nothing to do
- }
- }
- } else {
- error "untested xxx-c"
-
- }
-
- } else {
- #refvar to update is plain e.g ::p::${OID}::_ref::${prop}
- if {[llength $indices]} {
- if {[llength $indices] == 1} {
- set ${refvar}([lindex $indices 0]) [set SYNCVARIABLE([lindex $indices 0])]
- } else {
- lset ${refvar}([lindex $indices 0]) {*}[lrange $indices 1 end] [lindex [set SYNCVARIABLE([lindex $indices 0])] {*}[lrange $indices 1 end]]
- }
- lappend refs_updated $refvar
- } else {
- error "untested yyy"
- set $refvar $SYNCVARIABLE
- }
- }
- }
- } else {
- #vtraced non array, but could be an array element e.g ::p::${OID}::_ref::ARR(x)
- #
- foreach refvar $refvars {
- #puts stdout "\n\n \tsimplevariable:'$vtraced' examining REFVAR:'$refvar'"
- set refvar_tail [namespace tail $refvar]
- if {[string match "${prop}+*" $refvar_tail]} {
- #refvar to update is curried e.g ::p::${OID}::_ref::${prop}+x+y
- set ref_indices [lrange [split $refvar_tail +] 1 end]
-
- if {[llength $indices]} {
- #see if this update would affect this curried ref
- #1st see if we can short-circuit our comparison based on numeric-indices
- if {[string is digit -strict [join [concat $ref_indices $indices] ""]]} {
- #both sets of indices are purely numeric (no end end-1 etc)
- set rlen [llength $ref_indices]
- set ilen [llength $indices]
- set minlen [expr {min($rlen,$ilen)}]
- set matched_firstfew_indices 1 ;#assume the best
- for {set i 0} {$i < $minlen} {incr i} {
- if {[lindex $ref_indices $i] ne [lindex $indices $i]} {
- break ;#
- }
- }
- if {!$matched_firstfew_indices} {
- #update of this refvar not required
- #puts stderr "\t@@@1 SKIPPING refvar $refvar - indices don't match $ref_indices vs $indices"
- break ;#break to next refvar in the foreach loop
- }
- }
- #failed to short-circuit
-
- #just do a simple value comparison - some optimisations are possible, but perhaps unnecessary here
- set newval [lindex $SYNCVARIABLE $ref_indices]
- if {[set $refvar] ne $newval} {
- set $refvar $newval
- lappend refs_updated $refvar
- }
-
- } else {
- #we must be updating the entire variable - so this curried ref will either need to be updated or unset
- set newval [lindex $SYNCVARIABLE $ref_indices]
- if {[set ${refvar}] ne $newval} {
- set ${refvar} $newval
- lappend refs_updated $refvar
- }
- }
- } else {
- #refvar to update is plain e.g ::p::${OID}::_ref::${prop}
- if {[llength $indices]} {
- #error "untested zzz-a"
- set newval [lindex $SYNCVARIABLE $indices]
- if {[lindex [set $refvar] $indices] ne $newval} {
- lset ${refvar} $indices $newval
- lappend refs_updated $refvar
- }
- } else {
- if {[set ${refvar}] ne $SYNCVARIABLE} {
- set ${refvar} $SYNCVARIABLE
- lappend refs_updated $refvar
- }
- }
-
- }
-
- }
- }
- #--------------------------------------------------------------------------------------------------------------------------
-
- #!todo - manually fire $external_traces as appropriate - but somehow raise error if attempt to write/unset
-
- #reinstall the traces we stored at the beginning of this proc.
- foreach rv [array names traces] {
- if {$rv ni $refs_deleted} {
- foreach tinfo $traces($rv) {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
-
- #puts stderr "****** re-installing trace '$ops' on variable $rv cmd:$cmd"
- trace add variable $rv $ops $cmd
- }
- }
- }
- foreach rv [array names external_traces] {
- if {$rv ni $refs_deleted} {
- foreach tinfo $external_traces($rv) {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
- #trace add variable $rv $ops $cmd
- }
- }
- }
-
-
- return [list updated_refs $refs_updated]
-}
-
-#purpose: update all relevant references when context variable changed directly
-proc ::p::predator::propvar_write_TraceHandler {OID prop vtraced vidx op} {
- #note that $vtraced may have been upvared in calling scope - so could have any name! only use it for getting/setting values - don't rely on it's name in any other way.
- #we upvar it here instead of using uplevel - as presumably upvar is more efficient (don't have to wory about whether uplevelled script is bytecompiled etc) and also makes code simpler
-
- upvar $vtraced SYNCVARIABLE
- #puts stderr "\t>>>>>>>$ propvar_write_TraceHandler OID:$OID propertyname:'$prop' vtraced:'$vtraced' index:'$vidx' operation:$op"
- set t_info [trace vinfo $vtraced]
- foreach t_spec $t_info {
- set t_ops [lindex $t_spec 0]
- if {$op in $t_ops} {
- puts stderr "\t!!!!!!!! propvar_write_Tracehandler [lindex $t_spec 1]"
- }
- }
-
- #puts stderr -*-*-[info vars ::p::_ref::${OID}::[lindex $prop 0]+*]-*-*-
- #vtype = array | array-item | list | simple
-
- set refvars [::list]
-
- ############################
- #!!!NOTE!!! do not call 'info exists' on a propref here as it will trigger a read trace -which then pulls in the value from the (GET)prop function etc!!!
- #This would be extra cpu work - and sets the propref prematurely (breaking proper property-trace functionality plus vwaits on proprefs)
- #The alternative 'info vars' does not trigger traces
- if {[info vars ::p::${OID}::_ref::$prop] eq "::p::${OID}::_ref::$prop"} {
- #puts " **> lappending '::p::REF::${OID}::$prop'"
- lappend refvars ::p::${OID}::_ref::$prop ;#this is the basic unindexed reference we normally get when getting a standard property ref (e.g set ref [>obj . prop .])
- }
- ############################
-
- #lappend refvars ::p::${OID}::_ref::$prop ;#this is the basic unindexed reference we normally get when getting a standard property ref (e.g set ref [>obj . prop .])
- lappend refvars {*}[info vars ::p::${OID}::_ref::${prop}+*] ;#add any indexed references
-
-
- if {![llength $refvars]} {
- #puts stderr "\t%%%%%%%%%% no refvars for propvar_write_TraceHandler to update - short circuiting . OID:$OID prop:$prop"
- return
- }
-
-
- #puts stderr "*-*-*-*-*-* refvars \n- [join $refvars "\n- "]"
-
- #We are only interested in suppressing the pattern library's 'propref_trace_*' traces and 3rd party 'read' traces on refvars
- array set predator_traces [::list]
- #maintain two lists of external traces - as we need to temporarily deactivate all non-pattern read traces even if they are part of a more comprehensive trace..
- #ie for something like 'trace add variable someref {write read array} somefunc'
- # we need to remove and immediately reinstall it as a {write array} trace - and at the end of this procedure - reinstall it as the original {write read array} trace
- array set external_read_traces [::list] ;#pure read traces the library user may have added
- array set external_readetc_traces [::list] ;#read + something else traces the library user may have added
- foreach rv $refvars {
- #puts "--refvar $rv"
- foreach tinfo [trace info variable $rv] {
- #puts "##trace $tinfo"
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
- #!warning - assumes traces with single operation per handler.
- #write & unset traces on refvars need to be suppressed
- #we also need to be able to read certain refvars without triggering retrieval of underlying value in order to detect if changed.
- #if {$ops in {read write unset array}} {}
-
- if {[string match "::p::predator::propref_trace_*" $cmd]} {
- lappend predator_traces($rv) $tinfo
- trace remove variable $rv $ops $cmd
- #puts stderr "*-*-*-*-*-* removing $ops trace on $rv -> $cmd"
- } else {
- #other traces
- # puts "##trace $tinfo"
- if {"read" in $ops} {
- if {[llength $ops] == 1} {
- #pure read -
- lappend external_read_traces($rv) $tinfo
- trace remove variable $rv $ops $cmd
- } else {
- #mixed operation trace - remove and reinstall without the 'read'
- lappend external_readetc_traces($rv) $tinfo
- set other_ops [lsearch -all -inline -not $ops "read"]
- trace remove variable $rv $ops $cmd
- #reinstall trace for non-read operations only
- trace add variable $rv $other_ops $cmd
- }
- }
- }
- }
- }
-
-
- if {([array exists SYNCVARIABLE]) || (![info exists SYNCVARIABLE])} {
- #either the underlying variable is an array
- # OR - underlying variable doesn't exist - so we treat the property as an array because of the indexed access pattern
- set vtracedIsArray 1
- } else {
- set vtracedIsArray 0
- }
-
- #puts stderr "--------------------------------------------------\n\n"
-
- #puts stderr ">>>...----refsync-trace = $vtraced $op refvars:$refvars"
- #puts stderr ">>> [trace info variable $vtraced]"
- #puts "**write*********** propvar_write_TraceHandler $prop $vtraced $vidx $op"
- #puts "**write*********** refvars: $refvars"
-
- #!todo? unroll foreach into multiple foreaches within ifs?
- #foreach refvar $refvars {}
-
-
- #puts stdout "propvar_write_TraceHandler examining REFVAR $refvar"
- if {[string length $vidx]} {
- #indexable
- if {$vtracedIsArray} {
-
- foreach refvar $refvars {
- #puts stderr " - - a refvar $refvar vidx: $vidx"
- set tail [namespace tail $refvar]
- if {[string match "${prop}+*" $tail]} {
- #refvar is curried
- #only set if vidx matches curried index
- #!todo -review
- set idx [lrange [split $tail +] 1 end]
- if {$idx eq $vidx} {
- set newval [set SYNCVARIABLE($vidx)]
- if {[set $refvar] ne $newval} {
- set ${refvar} $newval
- }
- #puts stderr "=a.1=> updated $refvar"
- }
- } else {
- #refvar is simple
- set newval [set SYNCVARIABLE($vidx)]
- if {![info exists ${refvar}($vidx)]} {
- #new key for this array
- #puts stderr "\npropvar_write_TraceHandler------ about to call 'array set $refvar [::list $vidx [set SYNCVARIABLE($vidx)] ]' "
- array set ${refvar} [::list $vidx [set SYNCVARIABLE($vidx)] ]
- } else {
- set oldval [set ${refvar}($vidx)]
- if {$oldval ne $newval} {
- #puts stderr "\npropvar_write_TraceHandler------ about to call 'array set $refvar [::list $vidx [set SYNCVARIABLE($vidx)] ]' "
- array set ${refvar} [::list $vidx [set SYNCVARIABLE($vidx)] ]
- }
- }
- #puts stderr "=a.2=> updated ${refvar} $vidx"
- }
- }
-
-
-
- } else {
-
-
- foreach refvar $refvars {
- upvar $refvar internal_property_reference
- #puts stderr " - - b vidx: $vidx"
-
- #!? could be object not list??
- #!!but what is the difference between an object, and a list of object names which happens to only contain one object??
- #For predictability - we probably need to autodetect type on 1st write to o_prop either list, array or object (and maintain after unset operations)
- #There would still be an edge case of an initial write of a list of objects of length 1.
- if {([llength [set $SYNCVARIABLE]] ==1) && ([string range [set $SYNCVARIABLE] 0 0] eq ">")} {
- error "untested review!"
- #the o_prop is object-shaped
- #assumes object has a defaultmethod which accepts indices
- set newval [[set $SYNCVARIABLE] {*}$vidx]
-
- } else {
- set newval [lindex $SYNCVARIABLE {*}$vidx]
- #if {[set $refvar] ne $newval} {
- # set $refvar $newval
- #}
- if {$internal_property_reference ne $newval} {
- set internal_property_reference $newval
- }
-
- }
- #puts stderr "=b=> updated $refvar"
- }
-
-
- }
-
-
-
- } else {
- #no vidx
-
- if {$vtracedIsArray} {
-
-
- foreach refvar $refvars {
- set targetref_tail [namespace tail $refvar]
- set targetref_is_indexed [string match "${prop}+*" $targetref_tail]
-
-
- #puts stderr " - - c traced: $vtraced refvar:$refvar triggeringRef: $triggeringRef"
- if {$targetref_is_indexed} {
- #curried array item ref of the form ${prop}+x or ${prop}+x+y etc
-
- #unindexed write on a property that is acting as an array..
-
- #case a) If the underlying variable is actually an array - it will error upon attempt to write it like this - that's ok.
-
- #case b) If the underlying variable doesn't exist - perhaps a PropertyWrite will accept the unindexed write (e.g by asigning a default for the missing index).
- # we can't know here how this write affects other indexed traces on this property... hence we warn but do nothing.
- puts stderr "\tc.1 WARNING: write to property without 'array set'. op:'$op' refvar:'$refvar' prop:'$prop' \n\traw: propvar_write_TraceHandler $OID $prop $vtraced $vidx $op"
- } else {
- #How do we know what to write to array ref?
- puts stderr "\tc.2 WARNING: unimplemented/unused?"
- #error no_tests_for_branch
-
- #warning - this would trigger 3rd party unset traces which is undesirable for what is really a 'bookkeeping' operation
- #if this branch is actually useful - we probably need to step through the array and unset and set elements as appropriate
- array unset ${refvar}
- array set ${refvar} [array get SYNCVARIABLE]
- }
- }
-
-
-
- } else {
- foreach refvar $refvars {
- #puts stderr "\t\t_________________[namespace current]"
- set targetref_tail [namespace tail $refvar]
- upvar $refvar INTERNAL_REFERENCE_TO_PROPERTY__$targetref_tail
- set targetref_is_indexed [string match "${prop}+*" $targetref_tail]
-
- if {$targetref_is_indexed} {
- #puts "XXXXXXXXX vtraced:$vtraced"
- #reference curried with index(es)
- #we only set indexed refs if value has changed
- # - this not required to be consistent with standard list-containing variable traces,
- # as normally list elements can't be traced seperately anyway.
- #
-
-
- #only bother checking a ref if no setVia index
- # i.e some operation on entire variable so need to test synchronisation for each element-ref
- set targetref_indices [lrange [split $targetref_tail +] 1 end]
- set possiblyNewVal [lindex $SYNCVARIABLE {*}$targetref_indices]
- #puts stderr "YYYYYYYYY \[set \$refvar\]: [set $refvar] / possiblyNewVal: $possiblyNewVal"
- if {[set INTERNAL_REFERENCE_TO_PROPERTY__$targetref_tail] ne $possiblyNewVal} {
- set INTERNAL_REFERENCE_TO_PROPERTY__$targetref_tail $possiblyNewVal
- #puts stderr "=d1=> updated $refvar -> [uplevel 1 "lindex \[set $vtraced] $idx"]"
- }
-
-
- } else {
- #for consistency with standard traces on a list-containing variable, we perform the set even if the list value has not changed!
-
- #puts stderr "- d2 set"
- #puts "refvar: [set $refvar]"
- #puts "SYNCVARIABLE: $SYNCVARIABLE"
-
- #if {[set $refvar] ne $SYNCVARIABLE} {
- # set $refvar $SYNCVARIABLE
- #}
- if {[set INTERNAL_REFERENCE_TO_PROPERTY__$targetref_tail] ne $SYNCVARIABLE} {
- set INTERNAL_REFERENCE_TO_PROPERTY__$targetref_tail $SYNCVARIABLE
- }
-
- }
- }
-
-
- }
-
- }
-
-
-
-
- #reinstall the traces we stored at the beginning of this proc.
- foreach rv [array names predator_traces] {
- foreach tinfo $predator_traces($rv) {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
-
- #puts stderr "****** re-installing trace '$ops' on variable $rv cmd:$cmd"
- trace add variable $rv $ops $cmd
- }
- }
-
- foreach rv [array names external_traces] {
- foreach tinfo $external_traces($rv) {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
-
- #puts stderr "****** re-installing trace '$ops' on variable $rv cmd:$cmd"
- trace add variable $rv $ops $cmd
- }
- }
-
-
-
-}
-
-# end propvar_write_TraceHandler
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-#
-
-#returns 0 if method implementation not present for interface
-proc ::p::predator::method_chainhead {iid method} {
- #Interface proc
- # examine the existing command-chain
- set candidates [info commands ::p::${iid}::_iface::$method.*] ;#rough grab (info commands only allows basic pattern globbing - not a regex)
- set cmdchain [list]
-
- set re [string map [list %m% [string map {( \\( ) \\) . \\.} $method]] {^%m%.([0-9]+)$}]
- set maxversion 0
- #loop and test because it is possible there are unrelated commands (having a matching prefix with . character) which were caught in the glob.
- foreach test [lsort -dictionary $candidates] {
- set c [namespace tail $test]
- if {[regexp $re $c _match version]} {
- lappend cmdchain $c
- if {$version > $maxversion} {
- set maxversion $version
- }
- }
- }
- return $maxversion
-}
-
-
-
-
-
-#this returns a script that upvars vars for all interfaces on the calling object -
-# - must be called at runtime from a method
-proc ::p::predator::upvar_all {_ID_} {
- #::set OID [lindex $_ID_ 0 0]
- ::set OID [::lindex [::dict get $_ID_ i this] 0 0]
- ::set decl {}
- #[set ::p::${OID}::_meta::map]
- #[dict get [lindex [dict get $_ID_ i this] 0 1] map]
-
- ::upvar #0 ::p::${OID}::_meta::map MAP
- #puts stdout "\n\n -->-->-->--> _meta::map '$MAP' <-<-<-\n\n"
- #set iflist [::lindex [dict get [lindex [dict get $_ID_ i this] 0 1] map] 1 0]
-
- ::foreach ifid [dict get $MAP interfaces level0] {
- if {[::dict size [::set ::p::${ifid}::_iface::o_variables]]} {
- ::array unset nsvars
- ::array set nsvars [::list]
- ::dict for {vname vinfo} [::set ::p::${ifid}::_iface::o_variables] {
- ::set varspace [::dict get $vinfo varspace]
- ::lappend nsvars($varspace) $vname
- }
- #nsvars now contains vars grouped by varspace.
-
- ::foreach varspace [::array names nsvars] {
- if {$varspace eq ""} {
- ::set ns ::p::${OID}
- } else {
- if {[::string match "::*" $varspace]} {
- ::set ns $varspace
- } else {
- ::set ns ::p::${OID}::$varspace
- }
- }
-
- ::append decl "namespace upvar $ns "
- ::foreach vname [::set nsvars($varspace)] {
- ::append decl "$vname $vname "
- }
- ::append decl " ;\n"
- }
- ::array unset nsvars
- }
- }
- ::return $decl
-}
-
-#we need to use eval because it is potentially a multiline script returned by upvar_all (so can't just use {*} operator)
-proc ::p::predator::runtime_vardecls {} {
- set result "::eval \[::p::predator::upvar_all \$_ID_\]"
- #set result "::apply { {_ID_} ::p::predator::upvar_all } \$_ID_"
-
- #set result "::apply \[::list {} \[::p::predator::upvar_all \$_ID_\] \[namespace current\]\]"
- #set result "::interp eval {} \[::p::predator::upvar_all \$_ID_\]"
- #puts stdout "\t>>>[info level -1]\n\t>>>>>>>>>>>>>>>>>>>>> '$result'"
- return $result
-}
-
-
-
-
-
-
-#OBSOLETE!(?) - todo - move stuff out of here.
-proc ::p::predator::compile_interface {IFID caller_ID_} {
- upvar 0 ::p::${IFID}:: IFACE
-
- #namespace eval ::p::${IFID} {
- # namespace ensemble create
- #}
-
- #'namespace upvar' - from tip.tcl.tk #250: Efficient Access to Namespace Variables
-
- namespace upvar ::p::${IFID}::_iface o_propertyunset_handlers o_propertyunset_handlers o_variables o_variables o_properties o_properties o_methods o_methods o_unknown o_unknown o_varspace o_varspace o_varspaces o_varspaces
-
- #set varDecls {}
- #if {[llength $o_variables]} {
- # #puts "*********!!!! $vlist"
- # append varDecls "namespace upvar ::p::\[lindex \$_ID_ 0 0 \] "
- # foreach vdef $o_variables {
- # append varDecls "[lindex $vdef 0] [lindex $vdef 0] "
- # }
- # append varDecls \n
- #}
-
- #runtime gathering of vars from other interfaces.
- #append varDecls [runtime_vardecls]
-
- set varDecls [runtime_vardecls]
-
-
-
- #implement methods
-
- #!todo - avoid globs on iface array? maintain list of methods in another slot?
- #foreach {n mname} [array get IFACE m-1,name,*] {}
-
-
- #namespace eval ::p::${IFID}::_iface "namespace export {*}$o_methods" ;#make methods available as interface ensemble.
-
-
-
- #implement property getters/setters/unsetters
- #'setter' overrides
- #pw short for propertywrite
- foreach {n property} [array get IFACE pw,name,*] {
- if {[string length $property]} {
- #set property [lindex [split $n ,] end]
-
- #!todo - next_script
- #set next [::p::next_script "\[set ::p::\${_ID_}::(self)]" $IFID $property]
-
- set maxversion [::p::predator::method_chainhead $IFID (SET)$property]
- set chainhead [expr {$maxversion + 1}]
- set THISNAME (SET)$property.$chainhead ;#first version will be (SET)$property.1
-
- set next [::p::predator::next_script $IFID (SET)$property $THISNAME $caller_ID_] ;#?! caller_ID_ ??
-
- set body $IFACE(pw,body,$property)
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set body $varDecls\n[dict get $processed body]
- #puts stderr "\t\timplicit vardecls used for propertywrite $property on interface $IFID ##### \n $body"
- }
-
- #set body [string map [::list @this@ "\[lindex \${_ID_} 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
-
-
- set maxversion [::p::predator::method_chainhead $IFID $property]
- set headid [expr {$maxversion + 1}]
-
- proc ::p::${IFID}::_iface::(SET)$property.$headid [concat _ID_ $IFACE(pw,arg,$property)] $body
-
- interp alias {} ::p::${IFID}::_iface::(SET)$property {} ::p::${IFID}::_iface::(SET)$property.$headid
-
- #proc ::p::${IFID}::___system___write_$property [concat _ID_ $IFACE(pw,arg,$property)] $body
- }
- }
- #'unset' overrides
-
- dict for {property handler_info} $o_propertyunset_handlers {
-
- set body [dict get $handler_info body]
- set arraykeypattern [dict get $handler_info arraykeypattern] ;#array element pattern for unsetting individual elements in an array
-
- set maxversion [::p::predator::method_chainhead $IFID (UNSET)$property]
- set headid [expr {$maxversion + 1}]
-
- set THISNAME (UNSET)$property.$headid
-
- set next [::p::predator::next_script $IFID (UNSET)$property $THISNAME $caller_ID_] ;#?! caller_ID_ ???
-
-
-
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set body $varDecls\n[dict get $processed body]
- #puts stderr "\t\timplicit vardecls used for property unset $property on interface $IFID ##### \n $body"
-
- }
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
-
-
-
- #implement
- #always take arraykeypattern argument even though usually empty string (only used for unsetting individual array elements)
- if {[string trim $arraykeypattern] eq ""} {
- set arraykeypattern "_dontcare_"
- }
- proc ::p::${IFID}::_iface::(UNSET)$property.$headid [concat _ID_ $arraykeypattern] $body
-
-
- #chainhead pointer
- interp alias {} ::p::${IFID}::_iface::(UNSET)$property {} ::p::${IFID}::_iface::(UNSET)$property.$headid
- }
-
-
-
- interp alias {} ::p::${IFID}::(VIOLATE) {} ::p::internals::(VIOLATE)
-
- #the usual case will have no destructor - so use info exists to check.
-
- if {[info exists ::p::${IFID}::_iface::o_destructor_body]} {
- #!todo - chained destructors (support @next@).
- #set next [::p::next_script_destructor "\[lindex \$_ID_ 0 1\]" $IFID]
- set next NEXT
-
- set body [set ::p::${IFID}::_iface::o_destructor_body]
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set body $varDecls\n[dict get $processed body]
- #puts stderr "\t\t**********************implicit vardecls used for destructor on interface $IFID ##### \n $body"
- }
- #set body [::p::fixed_var_statements \n@IMPLICITDECLS@\n$body]
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
-
- proc ::p::${IFID}::___system___destructor _ID_ $body
- }
-
-
- if {[info exists o_unknown]} {
- #use 'apply' somehow?
- interp alias {} ::p::${IFID}::_iface::(UNKNOWN) {} ::p::${IFID}::_iface::$o_unknown
-
- #namespace eval ::p::${IFID}::_iface [list namespace unknown $o_unknown]
- }
-
-
- return
-}
-
-
-
-
-
-
-
-#'info args' - assuming arbitrary chain of 'interp aliases'
-proc ::p::predator::command_info_args {cmd} {
- if {[llength [set next [interp alias {} $cmd]]]} {
- set curriedargs [lrange $next 1 end]
-
- if {[catch {set arglist [info args [lindex $next 0]]}]} {
- set arglist [command_info_args [lindex $next 0]]
- }
- #trim curriedargs
- return [lrange $arglist [llength $curriedargs] end]
- } else {
- info args $cmd
- }
-}
-
-
-proc ::p::predator::do_next {_ID_ IFID mname nextArgs args} {
- if {[llength $args]} {
- tailcall ::p::${IFID}::_iface::$mname $_ID_ {*}$args
- } else {
- if {[llength $nextArgs] > 1} {
- set argVals [::list]
- set i 0
- foreach arg [lrange $nextArgs 1 end] {
- upvar 1 $arg $i
- if {$arg eq "args"} {
- #need to check if 'args' is actually available in caller
- if {[info exists $i]} {
- set argVals [concat $argVals [set $i]]
- }
- } else {
- lappend argVals [set $i]
- }
- }
- tailcall ::p::${IFID}::_iface::$mname $_ID_ {*}$argVals
- } else {
- tailcall ::p::${IFID}::_iface::$mname $_ID_
- }
- }
-}
-
-#----------------------------------------------------------------------------------------------
-proc ::p::predator::next_script {IFID method caller caller_ID_} {
-
- if {$caller eq "(CONSTRUCTOR).1"} {
- return [string map [list %cID% [list $caller_ID_] %ifid% $IFID %m% $method] {::p::predator::do_next_pattern_if $_ID_ %cID% %ifid% %m%}]
- } elseif {$caller eq "$method.1"} {
- #delegate to next interface lower down the stack which has a member named $method
- return [string map [list %ifid% $IFID %m% $method] {::p::predator::do_next_if $_ID_ %ifid% %m%}]
- } elseif {[string match "(GET)*.2" $caller]} {
- # .1 is the getprop procedure, .2 is the bottom-most PropertyRead.
-
- #jmn
- set prop [string trimright $caller 1234567890]
- set prop [string range $prop 5 end-1] ;#string leading (GET) and trailing .
-
- if {$prop in [dict keys [set ::p::${IFID}::_iface::o_properties]]} {
- #return [string map [list %ifid% $IFID %p% $prop ] {::p::%ifid%::_iface::(GET)%p%.1 $_ID_}]
- return [string map [list %ifid% $IFID %m% (GET)$prop.1 %nargs% [list]] {::p::predator::do_next $_ID_ %ifid% %m% [list %nargs%]}]
- } else {
- #we can actually have a property read without a property or a method of that name - but it could also match the name of a method.
- # (in which case it could return a different value depending on whether called via set [>obj . something .] vs >obj . something)
- return [string map [list %ifid% $IFID %m% $method] {::p::predator::do_next_if $_ID_ %ifid% %m%}]
- }
- } elseif {[string match "(SET)*.2" $caller]} {
- return [string map [list %ifid% $IFID %m% $method] {::p::predator::do_next_if $_ID_ %ifid% %m%}]
- } else {
- #this branch will also handle (SET)*.x and (GET)*.x where x >2
-
- #puts stdout "............next_script IFID:$IFID method:$method caller:$caller"
- set callerid [string range $caller [string length "$method."] end]
- set nextid [expr {$callerid - 1}]
-
- if {[catch {set nextArgs [info args ::p::${IFID}::_iface::$method.$nextid]} errMsg]} {
- #not a proc directly on this interface - presumably an alias made by something like linkcopy_interface.
- #puts ">>>>>>>>::p::predator::next_script IFID:$IFID caller:$caller aaaa@ $method.$nextid"
- set nextArgs [command_info_args ::p::${IFID}::_iface::$method.$nextid]
- }
-
- return [string map [list %ifid% $IFID %m% $method.$nextid %nargs% $nextArgs] {::p::predator::do_next $_ID_ %ifid% %m% [list %nargs%]}]
- }
-}
-
-proc ::p::predator::do_next_if {_ID_ IFID method args} {
- #puts "<>(::p::predator::do_next_if)<> '$_ID_' '$IFID' '$method' '$args' ((("
-
- #set invocants [dict get $_ID_ i]
- #set this_invocantdata [lindex [dict get $invocants this] 0]
- #lassign $this_invocantdata OID this_info
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set interfaces [dict get $MAP interfaces level0]
- set patterninterfaces [dict get $MAP interfaces level1]
-
- set L0_posn [lsearch $interfaces $IFID]
- if {$L0_posn == -1} {
- error "(::p::predator::do_next_if) called with interface not present at level0 for this object"
- } elseif {$L0_posn > 0} {
- #set ifid_next [lindex $interfaces $L0_posn-1] ;#1 lower in the iStack
- set lower_interfaces [lrange $interfaces 0 $L0_posn-1]
-
- foreach if_sub [lreverse $lower_interfaces] {
- if {[string match "(GET)*" $method]} {
- #do not test o_properties here! We need to call even if there is no underlying property on this interface
- #(PropertyRead without Property is legal. It results in dispatch to subsequent interface rather than property variable for this interface)
- # relevant test: higher_order_propertyread_chaining
- return [::p::${if_sub}::_iface::$method $_ID_ {*}$args]
- } elseif {[string match "(SET)*" $method]} {
- #must be called even if there is no matching $method in o_properties
- return [::p::${if_sub}::_iface::$method $_ID_ {*}$args]
- } elseif {[string match "(UNSET)*" $method]} {
- #review untested
- #error "do_next_if (UNSET) untested"
- #puts stderr "<>(::p::predator::do_next_if)<> (UNSET) called - dispatching to ::p::${if_sub}::_iface::$method with args:'$args'"
- return [::p::${if_sub}::_iface::$method $_ID_ {*}$args]
-
- } elseif {$method in [dict keys [set ::p::${if_sub}::_iface::o_methods]]} {
- if {[llength $args]} {
- #puts stdout "<>(::p::predator::do_next_if)<> - - - calling ::p::${if_sub}::_iface::$method on sub interface $if_sub with $args"
-
- #return [::p::${if_sub}::_iface::$method $_ID_ {*}$args]
- #tailcall ::p::${if_sub}::_iface::$method $_ID_ {*}$args
-
- #!todo - handle case where llength $args is less than number of args for subinterface command
- #i.e remaining args will need to be upvared to get values from calling scope (auto-set any values not explicitly set)
-
- #handle case where next interface has different arguments (masking of sub interfaces in the stack with function with different arity/signature)
- set head [interp alias {} ::p::${if_sub}::_iface::$method]
- set nextArgs [info args $head] ;#!todo - fix... head not necessarily a proc
- set argx [list]
- foreach a $nextArgs {
- lappend argx "\$a"
- }
-
- #todo - handle func a b args called with func "x" ie short on named vars so b needs to be upvared
-
- if {([llength $args] == [llength $nextArgs]) || ([lindex $nextArgs end] eq "args")} {
- tailcall apply [list $nextArgs [list ::p::${if_sub}::_iface::$method {*}$argx ]] $_ID_ {*}$args
- } else {
- #todo - upvars required for tail end of arglist
- tailcall apply [list $nextArgs [list ::p::${if_sub}::_iface::$method {*}$argx ]] $_ID_ {*}$args
- }
-
- } else {
- #auto-set: upvar vars from calling scope
- #!todo - robustify? alias not necessarily matching command name..
- set head [interp alias {} ::p::${if_sub}::_iface::$method]
-
-
- set nextArgs [info args $head] ;#!todo - fix... head not necessarily a proc
- if {[llength $nextArgs] > 1} {
- set argVals [::list]
- set i 0
- foreach arg [lrange $nextArgs 1 end] {
- upvar 1 $arg $i
- if {$arg eq "args"} {
- #need to check if 'args' is actually available in caller
- if {[info exists $i]} {
- set argVals [concat $argVals [set $i]]
- }
- } else {
- lappend argVals [set $i]
- }
- }
- #return [$head $_ID_ {*}$argVals]
- tailcall $head $_ID_ {*}$argVals
- } else {
- #return [$head $_ID_]
- tailcall $head $_ID_
- }
- }
- } elseif {$method eq "(CONSTRUCTOR)"} {
- #chained constructors will only get args if the @next@ caller explicitly provided them.
- puts stdout "!!!<>(::p::predator::do_next_if)<> CONSTRUCTOR CHAINED CALL via do_next_if _ID_:$_ID_ IFID:$IFID method:$method args:$args!!!"
- #return [::p::${if_sub}::_iface::(CONSTRUCTOR) $_ID_ {*}$args]
- xtailcall ::p::${if_sub}::_iface::(CONSTRUCTOR) $_ID_ {*}$args
- }
- }
- #no interfaces in the iStack contained a matching method.
- return
- } else {
- #no further interfaces in this iStack
- return
- }
-}
-
-
-#only really makes sense for (CONSTRUCTOR) calls.
-#_ID_ is the invocant data for the target. caller_ID_ is the invocant data for the calling(creating,cloning etc) pattern/class.
-proc ::p::predator::do_next_pattern_if {_ID_ caller_ID_ IFID method args} {
- #puts ")))) do_next_pattern_if _ID_:'$_ID_' IFID:'$IFID' method:'$method' args:'$args' ((("
-
- #set invocants [dict get $_ID_ i]
- #set this_invocant [lindex [dict get $invocants this] 0]
- #lassign $this_invocant OID this_info
- #set OID [lindex [dict get $invocants this] 0 0]
- #upvar #0 ::p::${OID}::_meta::map map
- #lassign [lindex $map 0] OID alias itemCmd cmd
-
-
- set caller_OID [lindex [dict get $caller_ID_ i this] 0 0]
- upvar #0 ::p::${caller_OID}::_meta::map callermap
-
- #set interfaces [lindex $map 1 0]
- set patterninterfaces [dict get $callermap interfaces level1]
-
- set L0_posn [lsearch $patterninterfaces $IFID]
- if {$L0_posn == -1} {
- error "do_next_pattern_if called with interface not present at level1 for this object"
- } elseif {$L0_posn > 0} {
-
-
- set lower_interfaces [lrange $patterninterfaces 0 $L0_posn-1]
-
- foreach if_sub [lreverse $lower_interfaces] {
- if {$method eq "(CONSTRUCTOR)"} {
- #chained constructors will only get args if the @next@ caller explicitly provided them.
- #puts stdout "!!! CONSTRUCTOR CHAINED CALL via do_next_pattern_if _ID_:$_ID_ IFID:$IFID method:$method args:$args!!!"
- tailcall ::p::${if_sub}::_iface::(CONSTRUCTOR) $_ID_ {*}$args
- }
- }
- #no interfaces in the iStack contained a matching method.
- return
- } else {
- #no further interfaces in this iStack
- return
- }
-}
-
-
-
-
-
-#------------------------------------------------------------------------------------------------
-
-
-
-
-
-#-------------------------------------------------------------------------------------
-#######################################################
-#######################################################
-#######################################################
-#######################################################
-#######################################################
-#######################################################
-#######################################################
-
-
-#!todo - can we just call new_object somehow to create this?
-
- #until we have a version of Tcl that doesn't have 'creative writing' scope issues -
- # - we should either explicity specify the whole namespace when setting variables or make sure we use the 'variable' keyword.
- # (see http://mini.net/tcl/1030 'Dangers of creative writing')
-namespace eval ::p::-1 {
- #namespace ensemble create
-
- namespace eval _ref {}
- namespace eval _meta {}
-
- namespace eval _iface {
- variable o_usedby
- variable o_open
- variable o_constructor
- variable o_variables
- variable o_properties
- variable o_methods
- variable o_definition
- variable o_varspace
- variable o_varspaces
-
- array set o_usedby [list i0 1] ;#!todo - review
- #'usedby' array the metaface is an exception. All objects use it - so we should list none of them rather than pointless updating of this value?
-
- set o_open 1
- set o_constructor [list]
- set o_variables [list]
- set o_properties [dict create]
- set o_methods [dict create]
- array set o_definition [list]
- set o_varspace ""
- set o_varspaces [list]
- }
-}
-
-
-#
-
-#interp alias {} ::p::internals::>metaface {} ::p::internals::predator [list [list -1 ::p::internals::>metaface item {}] {{} {}}]
-interp alias {} ::p::internals::>metaface {} ::p::internals::predator [list i [list this [list [list -1 ::p::internals::>metaface item {}]]] context {}]
-
-
-upvar #0 ::p::-1::_iface::o_definition def
-
-
-#! concatenate -> compose ??
-dict set ::p::-1::_iface::o_methods Concatenate {arglist {target args}}
-proc ::p::-1::Concatenate {_ID_ target args} {
- set invocants [dict get $_ID_ i]
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
- if {![string match "::*" $target]} {
- if {[set ns [uplevel 1 {namespace current}]] eq "::"} {
- set target ::$target
- } else {
- set target ${ns}::$target
- }
- }
- #add > character if not already present
- set target [namespace qualifiers $target]::>[string trimleft [namespace tail $target] >]
- set _target [string map {::> ::} $target]
-
- set ns [namespace qualifiers $target]
- if {$ns eq ""} {
- set ns "::"
- } else {
- namespace eval $ns {}
- }
-
- if {![llength [info commands $target]]} {
- #degenerate case - target does not exist
- #Probably just 1st of a set of Concatenate calls - so simply delegate to 'Clone'
- #review - should be 'Copy' so it has object state from namespaces and variables?
- return [::p::-1::Clone $_ID_ $target {*}$args]
-
- #set TARGETMAP [::p::predator::new_object $target]
- #lassign [lindex $TARGETMAP 0] target_ID target_cmd itemCmd
-
- } else {
- #set TARGETMAP [lindex [interp alias {} [namespace origin $target]] 1]
- set TARGETMAP [$target --]
-
- lassign [dict get $TARGETMAP invocantdata] target_ID target_cmd itemCmd
-
- #Merge lastmodified(?) level0 and level1 interfaces.
-
- }
-
- return $target
-}
-
-
-
-#Object's Base-Interface proc with itself as curried invocant.
-#interp alias {} ::p::-1::Create {} ::p::-1::_iface::Create $invocant
-#namespace eval ::p::-1 {namespace export Create}
-dict set ::p::-1::_iface::o_methods Define {arglist definitions}
-#define objects in one step
-proc ::p::-1::Define {_ID_ definitions} {
- set invocants [dict get $_ID_ i]
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- lassign [dict get $MAP invocantdata] OID alias default_method cmd
- set interfaces [dict get $MAP interfaces level0] ;#level-0 interfaces
- set patterns [dict get $MAP interfaces level1] ;#level-1 interfaces
-
- #!todo - change these to dicts; key=interface stack name value= a list of interfaces in the stack
- #set IFID0 [lindex $interfaces 0]
- #set IFID1 [lindex $patterns 0] ;#1st pattern
-
- #set IFID_TOP [lindex $interfaces end]
- set IFID_TOP [::p::predator::get_possibly_new_open_interface $OID]
-
- #set ns ::p::${OID}
-
- #set script [string map [list %definitions% $definitions] {
- # if {[lindex [namespace path] 0] ne "::p::-1"} {
- # namespace path [list ::p::-1 {*}[namespace path]]
- # }
- # %definitions%
- # namespace path [lrange [namespace path] 1 end]
- #
- #}]
-
- set script [string map [list %id% $_ID_ %definitions% $definitions] {
- set ::p::-1::temp_unknown [namespace unknown]
-
- namespace unknown [list ::apply {{funcname args} {::p::predator::redirect $funcname [list %id%] {*}$args}}]
-
-
- #namespace unknown [list ::apply { {funcname args} {if {![llength [info commands ::p::-1::$funcname]]} {::unknown $funcname {*}$args } else {::p::-1::$funcname [list %id%] {*}$args} }} ]
-
-
- %definitions%
-
-
- namespace unknown ${::p::-1::temp_unknown}
- return
- }]
-
-
-
- #uplevel 1 $script ;#this would run the script in the global namespace
- #run script in the namespace of the open interface, this allows creating of private helper procs
- #namespace inscope ::p::${IFID_TOP}::_iface $script ;#do not use tailcall here! Define belongs on the callstack
- #namespace inscope ::p::${OID} $script
- namespace eval ::p::${OID} $script
- #return $cmd
-}
-
-
-proc ::p::predator::redirect {func args} {
-
- #todo - review tailcall - tests?
- if {![llength [info commands ::p::-1::$func]]} {
- #error "invalid command name \"$func\""
- tailcall uplevel 1 [list ::unknown $func {*}$args]
- } else {
- tailcall uplevel 1 [list ::p::-1::$func {*}$args]
- }
-}
-
-
-#'immediate' constructor - this is really like a (VIOLATE) call.. todo - review.
-dict set ::p::-1::_iface::o_methods Construct {arglist {argpairs body args}}
-proc ::p::-1::Construct {_ID_ argpairs body args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- set interfaces [dict get $MAP interfaces level0]
- set iid_top [lindex $interfaces end]
- namespace upvar ::p::${iid_top}::_iface o_varspaces o_varspaces o_varspace o_varspace
-
- set ARGSETTER {}
- foreach {argname argval} $argpairs {
- append ARGSETTER "set $argname $argval\n"
- }
- #$_self (VIOLATE) $ARGSETTER$body
-
- set body $ARGSETTER\n$body
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- set body $varDecls\n[dict get $processed body]
- # puts stderr "\t runtime_vardecls in Construct $varDecls"
- }
-
- set next "\[error {next not implemented}\]"
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]"] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
-
- #namespace eval ::p::${iid_top} $body
-
- #return [apply [list {_ID_ args} $body ::p::${iid_top}::_iface] $_ID_]
- #does this handle Varspace before constructor?
- return [apply [list {_ID_ args} $body ::p::${OID} ] $_ID_ {*}$args]
-}
-
-
-
-
-
-#hacked optimized version of ::p::-1::Create for creating ::p::ifaces::>* objects
-namespace eval ::p::3 {}
-proc ::p::3::_create {child {OID "-2"}} {
- #puts stderr "::p::3::_create $child $OID"
- set _child [string map {::> ::} $child]
- if {$OID eq "-2"} {
- #set childmapdata [::p::internals::new_object $child]
- #set child_ID [lindex [dict get $childmapdata invocantdata] 0 ]
- set child_ID [lindex [dict get [::p::internals::new_object $child] invocantdata] 0]
- upvar #0 ::p::${child_ID}::_meta::map CHILDMAP
- } else {
- set child_ID $OID
- #set _childmap [::p::internals::new_object $child "" $child_ID]
- ::p::internals::new_object $child "" $child_ID
- upvar #0 ::p::${child_ID}::_meta::map CHILDMAP
- }
-
- #--------------
-
- set oldinterfaces [dict get $CHILDMAP interfaces]
- dict set oldinterfaces level0 [list 2]
- set modifiedinterfaces $oldinterfaces
- dict set CHILDMAP interfaces $modifiedinterfaces
-
- #--------------
-
-
-
-
- #puts stderr ">>>> creating alias for ::p::$child_ID"
- #puts stderr ">>>::p::3::_create $child $OID >>>[interp alias {} ::p::$child_ID]"
-
- #interp alias ::p::$child_ID already exists at this point - so calling here will do nothing!
- #interp alias {} ::p::$child_ID {} ::p::internals::predator [dict create i [dict create this [list [list $child_ID {} ]]]]
- #puts stderr ">>>[interp alias {} ::p::$child_ID]"
-
-
-
- #---------------
- namespace upvar ::p::2::_iface o_methods o_methods o_properties o_properties
- foreach method [dict keys $o_methods] {
- #todo - change from interp alias to context proc
- interp alias {} ::p::${child_ID}::$method {} ::p::2::_iface::$method
- }
- #namespace eval ::p::${child_ID} [list namespace export {*}$o_methods]
- #implement property even if interface already compiled because we need to create defaults for each new child obj.
- # also need to add alias on base interface
- #make sure we are only implementing properties from the current CREATOR
- dict for {prop pdef} $o_properties {
- #lassign $pdef prop default
- interp alias {} ::p::${child_ID}::$prop {} ::p::2::_iface::(GET)$prop
- interp alias {} ::p::${child_ID}::(GET)$prop {} ::p::2::_iface::(GET)$prop
-
- }
- ::p::2::_iface::(CONSTRUCTOR) [dict create i [dict create this [list [dict get $CHILDMAP invocantdata]]] context {}]
- #---------------
- #namespace eval ::p::${child_ID} "namespace ensemble create -command $_child"
- return $child
-}
-
-#configure -prop1 val1 -prop2 val2 ...
-dict set ::p::-1::_iface::o_methods Configure {arglist args}
-proc ::p::-1::Configure {_ID_ args} {
-
- #!todo - add tests.
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd this
-
- if {![expr {([llength $args] % 2) == 0}]} {
- error "expected even number of Configure args e.g '-property1 value1 -property2 value2'"
- }
-
- #Do a separate loop to check all the arguments before we run the property setting loop
- set properties_to_configure [list]
- foreach {argprop val} $args {
- if {!([string range $argprop 0 0] eq "-") || ([string length $argprop] < 2)} {
- error "expected Configure args in the form: '-property1 value1 -property2 value2'"
- }
- lappend properties_to_configure [string range $argprop 1 end]
- }
-
- #gather all valid property names for all level0 interfaces in the relevant interface stack
- set valid_property_names [list]
- set iflist [dict get $MAP interfaces level0]
- foreach id [lreverse $iflist] {
- set interface_property_names [dict keys [set ::p::${id}::_iface::o_properties]]
- foreach if_prop $interface_property_names {
- if {$if_prop ni $valid_property_names} {
- lappend valid_property_names $if_prop
- }
- }
- }
-
- foreach argprop $properties_to_configure {
- if {$argprop ni $valid_property_names} {
- error "Configure failed - no changes made. Unable to find property '$argprop' on object $this OID:'$OID' valid properties: $valid_property_names"
- }
- }
-
- set top_IID [lindex $iflist end]
- #args ok - go ahead and set all properties
- foreach {prop val} $args {
- set property [string range $prop 1 end]
- #------------
- #don't use property ref unnecessarily - leaves property refs hanging around which traces need to update
- #ie don't do this here: set [$this . $property .] $val
- #-------------
- ::p::${top_IID}::_iface::(SET)$property $_ID_ $val ;#equivalent to [$this . (SET)$property $val]
- }
- return
-}
-
-
-
-
-
-
-dict set ::p::-1::_iface::o_methods AddPatternInterface {arglist iid}
-proc ::p::-1::AddPatternInterface {_ID_ iid} {
- #puts stderr "!!!!!!!!!!!!!!! ::p::-1::AddPatternInterface $_ID_ $iid"
- if {![string is integer -strict $iid]} {
- error "adding interface by name not yet supported. Please use integer id"
- }
-
- set invocants [dict get $_ID_ i]
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
- #lassign [lindex $invocant 0] OID alias itemCmd cmd
-
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set existing_ifaces [dict get $MAP interfaces level1] ;#pattern interfaces
-
-
-
- #it is theoretically possible to have the same interface present multiple times in an iStack.
- # #!todo -review why/whether this is useful. should we disallow it and treat as an error?
-
- lappend existing_ifaces $iid
- #lset map {1 1} $existing_ifaces
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 $existing_ifaces
- dict set MAP interfaces $extracted_sub_dict
-
- #lset invocant {1 1} $existing_ifaces
-
-}
-
-
-#!todo - update usedby ??
-dict set ::p::-1::_iface::o_methods AddInterface {arglist iid}
-proc ::p::-1::AddInterface {_ID_ iid} {
- #puts stderr "::p::-1::AddInterface _ID_:$_ID_ iid:$iid"
- if {![string is integer -strict $iid]} {
- error "adding interface by name not yet supported. Please use integer id"
- }
-
-
- lassign [dict get $_ID_ i this] list_of_invocants_for_role_this ;#Although there is normally only 1 'this' element - it is a 'role' and the structure is nonetheless a list.
- set this_invocant [lindex $list_of_invocants_for_role_this 0]
-
- lassign $this_invocant OID _etc
-
- upvar #0 ::p::${OID}::_meta::map MAP
- set existing_ifaces [dict get $MAP interfaces level0]
-
- lappend existing_ifaces $iid
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 $existing_ifaces
- dict set MAP interfaces $extracted_sub_dict
- return [dict get $extracted_sub_dict level0]
-}
-
-
-
-# The 'Create' method on the meta-interface has 2 variants (CreateNew & CreateOverlay) provided to enhance code clarity for the application using the pattern module.
-# The 'Create' method could be used in all instances - but 'CreateNew' is designed for the case where the target/child object does not yet exist
-# and 'CreateOverlay' for the case where the target/child object already exists.
-# If the application writer follows the convention of using 'CreateNew' & 'CreateOverlay' instead of 'Create' - it should be more obvious where a particular object first comes into existence,
-# and it should reduce errors where the author was expecting to overlay an existing object, but accidentally created a new object.
-# 'CreateNew' will raise an error if the target already exists
-# 'CreateOverlay' will raise an error if the target object does not exist.
-# 'Create' will work in either case. Creating the target if necessary.
-
-
-#simple form:
-# >somepattern .. Create >child
-#simple form with arguments to the constructor:
-# >somepattern .. Create >child arg1 arg2 etc
-#complex form - specify more info about the target (dict keyed on childobject name):
-# >somepattern .. Create {>child {-id 1}}
-#or
-# >somepattern .. Create [list >child {-id 1 -somethingelse etc} >child2 {}]
-#complex form - with arguments to the contructor:
-# >somepattern .. Create [list >child {-id 1}] arg1 arg2 etc
-dict set ::p::-1::_iface::o_methods Create {arglist {target_spec args}}
-proc ::p::-1::Create {_ID_ target_spec args} {
- #$args are passed to constructor
- if {[llength $target_spec] ==1} {
- set child $target_spec
- set targets [list $child {}]
- } else {
- set targets $target_spec
- }
-
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
- set invocants [dict get $_ID_ i]
- set invocant_roles [dict keys $invocants] ;#usually the only invocant role present will be 'this' (single dispatch case)
-
- foreach {child target_spec_dict} $targets {
- #puts ">>>::p::-1::Create $_ID_ $child $args <<<"
-
-
-
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
-
-
-
-
- #puts ">>Create _ID_:$_ID_ child:$child args:$args map:$map OID:$OID"
-
- #child should already be fully ns qualified (?)
- #ensure it is has a pattern-object marker >
- #puts stderr ".... $child (nsqual: [namespace qualifiers $child])"
-
-
- lassign [dict get $MAP invocantdata] OID alias parent_defaultmethod cmd
- set interfaces [dict get $MAP interfaces level0] ;#level-0 interfaces
- set patterns [dict get $MAP interfaces level1] ;#level-1 interfaces
- #puts "parent: $OID -> child:$child Patterns $patterns"
-
- #todo - change to dict of interface stacks
- set IFID0 [lindex $interfaces 0]
- set IFID1 [lindex $patterns 0] ;#1st pattern
-
- #upvar ::p::${OID}:: INFO
-
- if {![string match {::*} $child]} {
- if {[set ns [uplevel 1 {namespace current}]] eq "::"} {
- set child ::$child
- } else {
- set child ${ns}::$child
- }
- }
-
-
- #add > character if not already present
- set child [namespace qualifiers $child]::>[string trimleft [namespace tail $child] >]
- set _child [string map {::> ::} $child]
-
- set ns [namespace qualifiers $child]
- if {$ns eq ""} {
- set ns "::"
- } else {
- namespace eval $ns {}
- }
-
-
- #maintain a record of interfaces created so that we can clean-up if we get an error during any of the Constructor calls.
- set new_interfaces [list]
-
- if {![llength $patterns]} {
- ##puts stderr "===> WARNING: no level-1 interfaces (patterns) on object $cmd when creating $child"
- #lappend patterns [::p::internals::new_interface $OID]
-
- #lset invocant {1 1} $patterns
- ##update our command because we changed the interface list.
- #set IFID1 [lindex $patterns 0]
-
- #set patterns [list [::p::internals::new_interface $OID]]
-
- #set patterns [list [::p::internals::new_interface]]
-
- #set patterns [list [set iid [expr {$::p::ID + 1}]]] ;#PREDICT the next object's id
- #set patterns [list [set iid [incr ::p::ID]]]
- set patterns [list [set iid [::p::get_new_object_id]]]
-
- #---------
- #set iface [::p::>interface .. Create ::p::ifaces::>$iid]
- #::p::-1::Create [list {caller ::p::3}] ::p::ifaces::>$iid
-
- #lappend new_interfaces [::p::3::_create ::p::ifaces::>$iid] ;#interface creation
- lappend new_interfaces [::p::3::_create ::p::ifaces::>$iid $iid]
-
- #---------
-
- #puts "??> p::>interface .. Create ::p::ifaces::>$iid"
- #puts "??> [::p::ifaces::>$iid --]"
- #set [$iface . UsedBy .]
- }
- set parent_patterndefaultmethod [dict get $MAP patterndata patterndefaultmethod]
-
- #if {![llength [info commands $child]]} {}
-
- if {[namespace which $child] eq ""} {
- #normal case - target/child does not exist
- set is_new_object 1
-
- if {[dict exists $target_spec_dict -id]} {
- set childmapdata [::p::internals::new_object $child "" [dict get $target_spec_dict -id]]
- } else {
- set childmapdata [::p::internals::new_object $child]
- }
- lassign [dict get $childmapdata invocantdata] child_ID child_alias child_defaultmethod
- upvar #0 ::p::${child_ID}::_meta::map CHILDMAP
-
-
-
- #child initially uses parent's level1 interface as it's level0 interface
- # child has no level1 interface until PatternMethods or PatternProperties are added
- # (or applied via clone; or via create with a parent with level2 interface)
- #set child_IFID $IFID1
-
- #lset CHILDMAP {1 0} [list $IFID1]
- #lset CHILDMAP {1 0} $patterns
-
- set extracted_sub_dict [dict get $CHILDMAP interfaces]
- dict set extracted_sub_dict level0 $patterns
- dict set CHILDMAP interfaces $extracted_sub_dict
-
- #why write back when upvared???
- #review
- set ::p::${child_ID}::_meta::map $CHILDMAP
-
- #::p::predator::remap $CHILDMAP
-
- #interp alias {} $child {} ::p::internals::predator $CHILDMAP
-
- #set child_IFID $IFID1
-
- #upvar ::p::${child_ID}:: child_INFO
-
- #!todo review
- #set n ::p::${child_ID}
- #if {![info exists ${n}::-->PATTERN_ANCHOR]} {
- # #puts stdout "### target:'$child' Creating ${n}::-->PATTERN_ANCHOR (unset trace to delete namespace '$n'"
- # #!todo - keep an eye on tip.tcl.tk #140 - 'Tracing Namespace Modification' - may be able to do away with this hack
- # set ${n}::-->PATTERN_ANCHOR "objects within this namespace will be deleted when this var is unset"
- # trace add variable ${n}::-->PATTERN_ANCHOR {unset} [list ::p::meta::clear_ns $n]
- #}
-
- set ifaces_added $patterns
-
- } else {
- #overlay/mixin case - target/child already exists
- set is_new_object 0
-
- #set CHILDMAP [lindex [interp alias {} [namespace origin $child]] 1]
- set childmapdata [$child --]
-
-
- #puts stderr " *** $cmd .. Create -> target $child already exists!!!"
- #puts " **** CHILDMAP: $CHILDMAP"
- #puts " ****"
-
- #puts stderr " ---> Properties: [$child .. Properties . names]"
- #puts stderr " ---> Methods: [$child .. Properties . names]"
-
- lassign [dict get $childmapdata invocantdata] child_ID child_alias child_default child_cmd
- upvar #0 ::p::${child_ID}::_meta::map CHILDMAP
-
- #set child_IFID [lindex $CHILDMAP 1 0 end]
- #if {$child_IFID != [set child_IFID [::p::internals::expand_interface $child_IFID]]} {
- # lset CHILDMAP {1 0} [concat [lindex $CHILDMAP 1 0] $child_IFID]
- # interp alias {} $child_cmd {} ::p::internals::predator $CHILDMAP
- #}
- ##!todo? - merge only 'open' parent interfaces onto 'open' target interfaces
- #::p::merge_interface $IFID1 $child_IFID
-
-
- set existing_interfaces [dict get $CHILDMAP interfaces level0]
- set ifaces_added [list]
- foreach p $patterns {
- if {$p ni $existing_interfaces} {
- lappend ifaces_added $p
- }
- }
-
- if {[llength $ifaces_added]} {
- #lset CHILDMAP {1 0} [concat [lindex $CHILDMAP 1 0] $ifaces_added]
- set extracted_sub_dict [dict get $CHILDMAP interfaces]
- dict set extracted_sub_dict level0 [concat $existing_interfaces $ifaces_added]
- dict set CHILDMAP interfaces $extracted_sub_dict
- #set ::p::${child_ID}::_meta::map $CHILDMAP ;#why?
- #::p::predator::remap $CHILDMAP
- }
- }
-
- #do not overwrite the child's defaultmethod value if the parent_patterndefaultmethod is empty
- if {$parent_patterndefaultmethod ne ""} {
- set child_defaultmethod $parent_patterndefaultmethod
- set CHILD_INVOCANTDATA [dict get $CHILDMAP invocantdata]
- lset CHILD_INVOCANTDATA 2 $child_defaultmethod
- dict set CHILDMAP invocantdata $CHILD_INVOCANTDATA
- #update the child's _ID_
- interp alias {} $child_alias {} ;#first we must delete it
- interp alias {} $child_alias {} ::p::internals::predator [list i [list this [list $CHILD_INVOCANTDATA] ] context {}]
-
- #! object_command was initially created as the renamed alias - so we have to do it again
- rename $child_alias $child
- trace add command $child rename [list $child .. Rename]
- }
- #!todo - review - dont we already have interp alias entries for every method/prop?
- #namespace eval ::p::${child_ID} "namespace ensemble create -command $_child"
-
-
-
-
-
- set constructor_failure 0 ;#flag to indicate abortion due to error during a constructor call.
-
-
-
- #------------------------------------------------------------------------------------
- #create snapshot of the object-namespaces variables to allow object state to be rolledback if any Constructor calls fail.
- # - All variables under the namespace - not just those declared as Variables or Properties
- # - use a namespace. For the usual case of success, we just namespace delete, and remove the COW traces.
- # - presumably this snapshot should be reasonably efficient even if variables hold large amounts of data, as Tcl implements Copy-On-Write.
-
- #NOTE - do not use the objectID as the sole identifier for the snapshot namespace.
- # - there may be multiple active snapshots for a single object if it overlays itself during a constructor,
- # and it may be that a failure of an inner overlay is deliberately caught and not considered reason to raise an error for the initial constructor call.
- # - we will use an ever-increasing snapshotid to form part of ns_snap
- set ns_snap "::p::snap::[incr ::p::snap::id]_$child_ID" ;#unique snapshot namespace for this call to Create.
-
- #!todo - this should look at child namespaces (recursively?)
- #!todo - this should examine any namespaces implied by the default 'varspace' value for all interfaces.
- # (some of these namespaces might not be descendants of the object's ::p::${child_ID} namespace)
-
- namespace eval $ns_snap {}
- foreach vname [info vars ::p::${child_ID}::*] {
- set shortname [namespace tail $vname]
- if {[array exists $vname]} {
- array set ${ns_snap}::${shortname} [array get $vname]
- } elseif {[info exists $vname]} {
- set ${ns_snap}::${shortname} [set $vname]
- } else {
- #variable exists without value (e.g created by 'variable' command)
- namespace eval $ns_snap [list variable $shortname] ;#create the variable without value, such that it is present, but does not 'info exist'
- }
- }
- #------------------------------------------------------------------------------------
-
-
-
-
-
-
-
-
-
- #puts "====>>> ifaces_added $ifaces_added"
- set idx 0
- set idx_count [llength $ifaces_added]
- set highest_constructor_IFID ""
- foreach IFID $ifaces_added {
- incr idx
- #puts "--> adding iface $IFID "
- namespace upvar ::p::${IFID}::_iface o_usedby o_usedby o_open o_open o_methods o_methods o_properties o_properties o_variables o_variables o_unknown o_unknown o_varspace o_varspace o_varspaces o_varspaces
-
- if {[llength $o_varspaces]} {
- foreach vs $o_varspaces {
- #ensure all varspaces for the interface exists so that the 'namespace upvar' entries in methods etc will work.
- if {[string match "::*" $vs]} {
- namespace eval $vs {} ;#an absolute path to a namespace which may not be under the object's namespace at all.
- } else {
- namespace eval ::p::${child_ID}::$vs {}
- }
- }
- }
-
- if {$IFID != 2} {
- #>ifinfo interface always has id 2 and is used by all interfaces - no need to add everything to its usedby list.
- if {![info exists o_usedby(i$child_ID)]} {
- set o_usedby(i$child_ID) $child_alias
- }
-
- #compile and close the interface only if it is shared
- if {$o_open} {
- ::p::predator::compile_interface $IFID $_ID_ ;#params: IFID , caller_ID_
- set o_open 0
- }
- }
-
-
-
- package require struct::set
-
- set propcmds [list]
- foreach cmd [info commands ::p::${IFID}::_iface::(GET)*] {
- set cmd [namespace tail $cmd]
- #may contain multiple results for same prop e.g (GET)x.3
- set cmd [string trimright $cmd 0123456789]
- set cmd [string trimright $cmd .] ;#do separately in case cmd name also contains numerals
- lappend propcmds [string range $cmd 5 end] ;#don't worry about dupes here.
- }
- set propcmds [struct::set union $propcmds] ;#a way to get rid of dupes.
- #$propcmds now holds all Properties as well as PropertyReads with no corresponding Property on this interface.
- foreach property $propcmds {
- #puts "\n\n ::p::${child_ID}::$property --->>>>>>>>>>>> ::p::${IFID}::_iface::(GET)$property \n"
- interp alias {} ::p::${child_ID}::(GET)$property {} ::p::${IFID}::_iface::(GET)$property ;#used by property reference traces
- interp alias {} ::p::${child_ID}::$property {} ::p::${IFID}::_iface::(GET)$property
- }
-
- set propcmds [list]
- foreach cmd [info commands ::p::${IFID}::_iface::(SET)*] {
- set cmd [namespace tail $cmd]
- #may contain multiple results for same prop e.g (GET)x.3
- set cmd [string trimright $cmd 0123456789]
- set cmd [string trimright $cmd .] ;#do separately in case cmd name also contains numerals
- lappend propcmds [string range $cmd 5 end] ;#don't worry about dupes here.
- }
- set propcmds [struct::set union $propcmds] ;#a way to get rid of dupes.
- #$propcmds now holds all Properties as well as PropertyReads with no corresponding Property on this interface.
- foreach property $propcmds {
- interp alias {} ::p::${child_ID}::(SET)$property {} ::p::${IFID}::_iface::(SET)$property ;#used by property reference traces
- }
-
-
- foreach method [dict keys $o_methods] {
- set arglist [dict get $o_methods $method arglist]
- set argvals ""
- foreach argspec $arglist {
- if {[llength $argspec] == 2} {
- set a [lindex $argspec 0]
- } else {
- set a $argspec
- }
-
- if {$a eq "args"} {
- append argvals " \{*\}\$args"
- } else {
- append argvals " \$$a"
- }
- }
- set argvals [string trimleft $argvals]
-
- #interp alias {} ::p::${child_ID}::$method {} ::p::${IFID}::_iface::$method
-
- #this proc directly on the object is not *just* a forwarding proc
- # - it provides a context in which the 'uplevel 1' from the running interface proc runs
- #This (in 2018) is faster than a namespace alias forward to an interface proc which used apply to run in the dynamically calculated namespace (it seems the dynamic namespace stopped it from byte-compiling?)
-
- #proc calls the method in the interface - which is an interp alias to the head of the implementation chain
-
-
- proc ::p::${child_ID}::$method [list _ID_ {*}$arglist] [subst {
- ::p::${IFID}::_iface::$method \$_ID_ $argvals
- }]
-
- #proc ::p::${child_ID}::$method [list _ID_ {*}$arglist] [string map [list @m@ $method @ID@ $IFID @argvals@ $argvals] {
- # ::p::@ID@::_iface::@m@ $_ID_ @argvals@
- #}]
-
-
- }
-
- #namespace eval ::p::${child_ID} [list namespace export {*}$o_methods]
-
- #implement property even if interface already compiled because we need to create defaults for each new child obj.
- # also need to add alias on base interface
- #make sure we are only implementing properties from the current CREATOR
- dict for {prop pdef} $o_properties {
- set varspace [dict get $pdef varspace]
- if {![string length $varspace]} {
- set ns ::p::${child_ID}
- } else {
- if {[string match "::*" $varspace]} {
- set ns $varspace
- } else {
- set ns ::p::${child_ID}::$varspace
- }
- }
- if {[dict exists $pdef default]} {
- if {![info exists ${ns}::o_$prop]} {
- #apply CREATORS defaults - don't trash existing state for matching property (only apply if var unset)
- set ${ns}::o_$prop [dict get $pdef default]
- }
- }
- #! May be replaced by a method with the same name
- if {$prop ni [dict keys $o_methods]} {
- interp alias {} ::p::${child_ID}::$prop {} ::p::${IFID}::_iface::(GET)$prop
- }
- interp alias {} ::p::${child_ID}::(GET)$prop {} ::p::${IFID}::_iface::(GET)$prop
- interp alias {} ::p::${child_ID}::(SET)$prop {} ::p::${IFID}::_iface::(SET)$prop
- }
-
-
-
- #variables
- #foreach vdef $o_variables {
- # if {[llength $vdef] == 2} {
- # #there is a default value defined.
- # lassign $vdef v default
- # if {![info exists ::p::${child_ID}::$v]} {
- # set ::p::${child_ID}::$v $default
- # }
- # }
- #}
- dict for {vname vdef} $o_variables {
- if {[dict exists $vdef default]} {
- #there is a default value defined.
- set varspace [dict get $vdef varspace]
- if {$varspace eq ""} {
- set ns ::p::${child_ID}
- } else {
- if {[string match "::*" $varspace]} {
- set ns $varspace
- } else {
- set ns ::p::${child_ID}::$varspace
- }
- }
- set ${ns}::$vname [dict get $vdef default]
- }
- }
-
-
- #!todo - review. Write tests for cases of multiple constructors!
-
- #We don't want to the run constructor for each added interface with the same set of args!
- #run for last one - rely on constructor authors to use @next@ properly?
- if {[llength [set ::p::${IFID}::_iface::o_constructor]]} {
- set highest_constructor_IFID $IFID
- }
-
- if {$idx == $idx_count} {
- #we are processing the last interface that was added - now run the latest constructor found
- if {$highest_constructor_IFID ne ""} {
- #at least one interface has a constructor
- if {[llength [set ::p::${highest_constructor_IFID}::_iface::o_constructor]]} {
- #puts ">>!! running constructor ifid:$highest_constructor_IFID child: $CHILDMAP"
- if {[catch {::p::${highest_constructor_IFID}::_iface::(CONSTRUCTOR) [dict create i [dict create this [list [dict get $CHILDMAP invocantdata] ] ]] {*}$args} constructor_error]} {
- set constructor_failure 1
- set constructor_errorInfo $::errorInfo ;#cache it immediately.
- break
- }
- }
- }
- }
-
- if {[info exists o_unknown]} {
- interp alias {} ::p::${IFID}::_iface::(UNKNOWN) {} ::p::${IFID}::_iface::$o_unknown
- interp alias {} ::p::${child_ID}::(UNKNOWN) {} ::p::${child_ID}::$o_unknown
-
-
- #interp alias {} ::p::${IFID}::_iface::(UNKNOWN) {} ::p::${child_ID}::$o_unknown
- #namespace eval ::p::${IFID}::_iface [list namespace unknown $o_unknown]
- #namespace eval ::p::${child_ID} [list namespace unknown $o_unknown]
- }
- }
-
- if {$constructor_failure} {
- if {$is_new_object} {
- #is Destroy enough to ensure that no new interfaces or objects were left dangling?
- $child .. Destroy
- } else {
- #object needs to be returned to a sensible state..
- #attempt to rollback all interface additions and object state changes!
- puts "!!!!!!!!!!!!!!!!>>>constructor rollback object $child_ID \n\n\n\n"
- #remove variables from the object's namespace - which don't exist in the snapshot.
- set snap_vars [info vars ${ns_snap}::*]
- puts "ns_snap '$ns_snap' vars'${snap_vars}'"
- foreach vname [info vars ::p::${child_ID}::*] {
- set shortname [namespace tail $vname]
- if {"${ns_snap}::$shortname" ni "$snap_vars"} {
- #puts "--- >>>>> unsetting $shortname "
- unset -nocomplain $vname
- }
- }
-
- #restore variables from snapshot - but try to do so with minimal writes (don't want to trigger any unnecessary traces)
- #values of vars may also have Changed
- #todo - consider traces? what is the correct behaviour?
- # - some application traces may have fired before the constructor error occurred.
- # Should the rollback now also trigger traces?
- #probably yes.
-
- #we need to test both source and dest var for arrayness - as the failed constructor could have changed the variable type, not just the value
- foreach vname $snap_vars {
- #puts stdout "@@@@@@@@@@@ restoring $vname"
- #flush stdout
-
-
- set shortname [namespace tail $vname]
- set target ::p::${child_ID}::$shortname
- if {$target in [info vars ::p::${child_ID}::*]} {
- set present 1 ;#variable exists in one of 3 forms; array, simple, or 'declared only'
- } else {
- set present 0
- }
-
- if {[array exists $vname]} {
- #restore 'array' variable
- if {!$present} {
- array set $target [array get $vname]
- } else {
- if {[array exists $target]} {
- #unset superfluous elements
- foreach key [array names $target] {
- if {$key ni [array names $vname]} {
- array unset $target $key
- }
- }
- #.. and write only elements that have changed.
- foreach key [array names $vname] {
- if {[set ${target}($key)] ne [set ${vname}($key)]} {
- set ${target}($key) [set ${vname}($key)]
- }
- }
- } else {
- #target has been changed to a simple variable - unset it and recreate the array.
- unset $target
- array set $target [array get $vname]
- }
- }
- } elseif {[info exists $vname]} {
- #restore 'simple' variable
- if {!$present} {
- set $target [set $vname]
- } else {
- if {[array exists $target]} {
- #target has been changed to array - unset it and recreate the simple variable.
- unset $target
- set $target [set $vname]
- } else {
- if {[set $target] ne [set $vname]} {
- set $target [set $vname]
- }
- }
- }
- } else {
- #restore 'declared' variable
- if {[array exists $target] || [info exists $target]} {
- unset -nocomplain $target
- }
- namespace eval ::p::${child_ID} [list variable $shortname]
- }
- }
- }
- namespace delete $ns_snap
- return -code error -errorinfo "oid:${child_ID} constructor_failure for IFID:${IFID}\n$constructor_errorInfo" $constructor_error
- }
- namespace delete $ns_snap
-
- }
-
-
-
- return $child
-}
-
-dict set ::p::-1::_iface::o_methods Clone {arglist {clone args}}
-#A cloned individual doesn't have the scars of its parent. i.e values (state) not *copied*
-# (new 'clean' object with same structure. values as set by constructor or *specified by defaults*)
-# Also: Any 'open' interfaces on the parent become closed on clone!
-proc ::p::-1::Clone {_ID_ clone args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- set invocants [dict get $_ID_ i]
- lassign [dict get $MAP invocantdata] OID alias parent_defaultmethod cmd
-
- set _cmd [string map {::> ::} $cmd]
- set tail [namespace tail $_cmd]
-
-
- #obsolete?
- ##set IFID0 [lindex $map 1 0 end]
- #set IFID0 [lindex [dict get $MAP interfaces level0] end]
- ##set IFID1 [lindex $map 1 1 end]
- #set IFID1 [lindex [dict get $MAP interfaces level1] end]
-
-
- if {![string match "::*" $clone]} {
- if {[set ns [uplevel 1 {namespace current}]] eq "::"} {
- set clone ::$clone
- } else {
- set clone ${ns}::$clone
- }
- }
-
-
- set clone [namespace qualifiers $clone]::>[string trimleft [namespace tail $clone] >]
- set _clone [string map {::> ::} $clone]
-
-
- set cTail [namespace tail $_clone]
-
- set ns [namespace qualifiers $clone]
- if {$ns eq ""} {
- set ns "::"
- }
-
- namespace eval $ns {}
-
-
- #if {![llength [info commands $clone]]} {}
- if {[namespace which $clone] eq ""} {
- set clonemapdata [::p::internals::new_object $clone]
- } else {
- #overlay/mixin case - target/clone already exists
- #set CLONEMAP [lindex [interp alias {} [namespace origin $clone]] 1]
- set clonemapdata [$clone --]
- }
- set clone_ID [lindex [dict get $clonemapdata invocantdata] 0]
-
- upvar #0 ::p::${clone_ID}::_meta::map CLONEMAP
-
-
- #copy patterndata element of MAP straight across
- dict set CLONEMAP patterndata [dict get $MAP patterndata]
- set CLONE_INVOCANTDATA [dict get $CLONEMAP invocantdata]
- lset CLONE_INVOCANTDATA 2 $parent_defaultmethod
- dict set CLONEMAP invocantdata $CLONE_INVOCANTDATA
- lassign $CLONE_INVOCANTDATA clone_ID clone_alias clone_defaultmethod clone
-
- #update the clone's _ID_
- interp alias {} $clone_alias {} ;#first we must delete it
- interp alias {} $clone_alias {} ::p::internals::predator [list i [list this [list $CLONE_INVOCANTDATA] ] context {}]
-
- #! object_command was initially created as the renamed alias - so we have to do it again
- rename $clone_alias $clone
- trace add command $clone rename [list $clone .. Rename]
-
-
-
-
- #obsolete?
- #upvar ::p::${clone_ID}:: clone_INFO
- #upvar ::p::${IFID0}:: IFACE ;#same interface on predecessor(self) and clone.
- #upvar ::p::${OID}:: INFO
-
-
- array set clone_INFO [array get INFO]
-
- array set ::p::${clone_ID}::_iface::o_usedby [list] ;#'usedby'
-
-
- #!review!
- #if {![catch {set itemCmd $IFACE(m-1,name,item)}]} {
- #puts "***************"
- #puts "clone"
- #parray IFINFO
- #puts "***************"
- #}
-
- #we need the parent(s) in order to 'clone'??? - probably, as the defs are usually there unless the object was created with ad-hoc methods/props directly from ::>pattern
-
-
- #clone's interface maps must be a superset of original's
- foreach lev {0 1} {
- #set parent_ifaces [lindex $map 1 $lev]
- set parent_ifaces [dict get $MAP interfaces level$lev]
-
- #set existing_ifaces [lindex $CLONEMAP 1 $lev]
- set existing_ifaces [dict get $CLONEMAP interfaces level$lev]
-
- set added_ifaces_$lev [list]
- foreach ifid $parent_ifaces {
- if {$ifid ni $existing_ifaces} {
-
- #interface must not remain extensible after cloning.
- if {[set ::p::${ifid}::_iface::o_open]} {
- ::p::predator::compile_interface $ifid $_ID_
- set ::p::${ifid}::_iface::o_open 0
- }
-
-
-
- lappend added_ifaces_$lev $ifid
- #clone 'uses' all it's predecessor's interfaces, so update each interface's 'usedby' list.
- set ::p::${ifid}::_iface::o_usedby(i$clone_ID) $clone
- }
- }
- set extracted_sub_dict [dict get $CLONEMAP interfaces]
- dict set extracted_sub_dict level$lev [concat $existing_ifaces [set added_ifaces_$lev]]
- dict set CLONEMAP interfaces $extracted_sub_dict
- #lset CLONEMAP 1 $lev [concat $existing_ifaces [set added_ifaces_$lev]]
- }
-
- #interp alias {} ::p::${IFID0}::(VIOLATE) {} ::p::internals::(VIOLATE)
-
-
- #foreach *added* level0 interface..
- foreach ifid $added_ifaces_0 {
- namespace upvar ::p::${ifid}::_iface o_methods o_methods o_properties o_properties o_variables o_variables o_constructor o_constructor o_unknown o_unknown
-
-
- dict for {prop pdef} $o_properties {
- #lassign $pdef prop default
- if {[dict exists $pdef default]} {
- set varspace [dict get $pdef varspace]
- if {$varspace eq ""} {
- set ns ::p::${clone_ID}
- } else {
- if {[string match "::*" $varspace]} {
- set ns $varspace
- } else {
- set ns ::p::${clone_ID}::$varspace
- }
- }
-
- if {![info exists ${ns}::o_$prop]} {
- #apply CREATORS defaults - don't trash existing state for matching property (only apply if var unset)
- set ${ns}::o_$prop [dict get $pdef default]
- }
- }
-
- #! May be replaced by method of same name
- if {[namespace which ::p::${clone_ID}::$prop] eq ""} {
- interp alias {} ::p::${clone_ID}::$prop {} ::p::${ifid}::_iface::(GET)$prop
- }
- interp alias {} ::p::${clone_ID}::(GET)$prop {} ::p::${ifid}::_iface::(GET)$prop
- interp alias {} ::p::${clone_ID}::(SET)$prop {} ::p::${ifid}::_iface::(SET)$prop
- }
-
- #variables
- dict for {vname vdef} $o_variables {
- if {[dict exists $vdef default]} {
- set varspace [dict get $vdef varspace]
- if {$varspace eq ""} {
- set ns ::p::${clone_ID}
- } else {
- if {[string match "::*" $varspace]} {
- set ns $varspace
- } else {
- set ns ::p::${clone_ID}::$varspace
- }
- }
- if {![info exists ${ns}::$vname]} {
- set ::p::${clone_ID}::$vname [dict get $vdef default]
- }
- }
- }
-
-
- #update the clone object's base interface to reflect the new methods.
- #upvar 0 ::p::${ifid}:: IFACE
- #set methods [list]
- #foreach {key mname} [array get IFACE m-1,name,*] {
- # set method [lindex [split $key ,] end]
- # interp alias {} ::p::${clone_ID}::$method {} ::p::${ifid}::_iface::$method $CLONEMAP
- # lappend methods $method
- #}
- #namespace eval ::p::${clone_ID} [list namespace export {*}$methods]
-
-
- foreach method [dict keys $o_methods] {
-
- set arglist [dict get $o_methods $method arglist]
- set argvals ""
- foreach argspec $arglist {
- if {[llength $argspec] == 2} {
- set a [lindex $argspec 0]
- } else {
- set a $argspec
- }
-
- if {$a eq "args"} {
- append argvals " \{*\}\$args"
- } else {
- append argvals " \$$a"
- }
- }
- set argvals [string trimleft $argvals]
- #interp alias {} ::p::${clone_ID}::$method {} ::p::${ifid}::_iface::$method
-
-
- #this proc directly on the object is not *just* a forwarding proc
- # - it provides a context in which the 'uplevel 1' from the running interface proc runs
- #This (in 2018) is faster than a namespace alias forward to an interface proc which used apply to run in the dynamically calculated namespace (it seems the dynamic namespace stopped it from byte-compiling?)
-
- #proc calls the method in the interface - which is an interp alias to the head of the implementation chain
- proc ::p::${clone_ID}::$method [list _ID_ {*}$arglist] [subst {
- ::p::${ifid}::_iface::$method \$_ID_ $argvals
- }]
-
- }
- #namespace eval ::p::${clone_ID} [list namespace export {*}$o_methods]
-
-
- if {[info exists o_unknown]} {
- #interp alias {} ::p::${IID}::_iface::(UNKNOWN) {} ::p::${clone_ID}::$o_unknown
- interp alias {} ::p::${IID}::_iface::(UNKNOWN) {} ::p::${IID}::_iface::$o_unknown
- interp alias {} ::p::${clone_ID}::(UNKNOWN) {} ::p::${clone_ID}::$o_unknown
-
- #namespace eval ::p::${IID}::_iface [list namespace unknown $o_unknown]
- #namespace eval ::p::${clone_ID} [list namespace unknown $o_unknown]
-
- }
-
-
- #2021
- #Consider >parent with constructor that sets height
- #.eg >parent .. Constructor height {
- # set o_height $height
- #}
- #>parent .. Create >child 5
- # - >child has height 5
- # now when we peform a clone operation - it is the >parent's constructor that will run.
- # A clone will get default property and var values - but not other variable values unless the constructor sets them.
- #>child .. Clone >fakesibling 6
- # - >sibling has height 6
- # Consider if >child had it's own constructor created with .. Construct prior to the clone operation.
- # The >child's constructor didn't run - even though we created a >fakesibling - because the paren'ts one ran instead.
- # If we now add a constructor to >fakesibling - and put @next@ for constructor chaining...
- # when we now do >sibling .. Create >grandchild
- # - The constructor on >sibling runs first but chains to >child - the cloner aunt/uncle of the >grandchild
- # (while the calling order can't be changed - the positioning of @next@ tag in the contructor can allow code to run before and/or after the chained constructors and chaining can be disabled by providing a constructor without this tag.)
- # However - the args supplied in the >clone operation don't get either constructor running on the >grandchild
- #(though other arguments can be manually passed)
- # #!review - does this make sense? What if we add
- #
- #constructor for each interface called after properties initialised.
- #run each interface's constructor against child object, using the args passed into this clone method.
- if {[llength [set constructordef [set o_constructor]]]} {
- #error
- puts "!!!!!> running constructor for ifid:$ifid on clone:$clone_ID"
- ::p::${ifid}::_iface::(CONSTRUCTOR) [dict create i [dict create this [list [dict get $CLONEMAP invocantdata]] ]] {*}$args
-
- }
-
- }
-
-
- return $clone
-
-}
-
-
-
-interp alias {} ::p::-1::constructor {} ::p::-1::Constructor ;#for Define compatibility (snit?)
-dict set ::p::-1::_iface::o_methods Constructor {arglist {arglist body}}
-proc ::p::-1::Constructor {_ID_ arglist body} {
- set invocants [dict get $_ID_ i]
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
- #lassign [lindex $invocant 0 ] OID alias itemCmd cmd
-
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- set patterns [dict get $MAP interfaces level1]
- set iid_top [lindex $patterns end] ;#!todo - choose 'open' interface to expand.
- set iface ::p::ifaces::>$iid_top
-
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- #no existing pattern - create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- #set iid_top [::p::get_new_object_id]
-
- #the >interface constructor takes a list of IDs for o_usedby
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top [list $OID]]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat $patterns $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat $patterns $iid_top]
-
- #::p::predator::remap $invocant
- }
- set IID $iid_top
-
- namespace upvar ::p::${IID}::_iface o_open o_open o_constructor o_constructor o_varspace o_varspace o_varspaces o_varspaces
-
-
- # examine the existing command-chain
- set maxversion [::p::predator::method_chainhead $IID (CONSTRUCTOR)]
- set headid [expr {$maxversion + 1}]
- set THISNAME (CONSTRUCTOR).$headid ;#first version will be $method.1
-
- set next [::p::predator::next_script $IID (CONSTRUCTOR) $THISNAME $_ID_]
-
- #set varspaces [::pattern::varspace_list]
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
-
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set varDecls [::p::predator::runtime_vardecls]
- set body $varDecls\n[dict get $processed body]
- #puts stderr "\t runtime_vardecls in Constructor $varDecls"
- }
-
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
- #puts stderr ----
- #puts stderr $body
- #puts stderr ----
-
- proc ::p::${IID}::_iface::(CONSTRUCTOR).$headid [concat _ID_ $arglist] $body
- interp alias {} ::p::${IID}::_iface::(CONSTRUCTOR) {} ::p::${IID}::_iface::(CONSTRUCTOR).$headid
-
-
-
- set o_constructor [list $arglist $body]
- set o_open 1
-
- return
-}
-
-
-
-dict set ::p::-1::_iface::o_methods UsedBy {arglist {}}
-proc ::p::-1::UsedBy {_ID_} {
- return [array get ::p::[lindex [dict get $_ID_ i this] 0 0]::_iface::o_usedby]
-}
-
-
-dict set ::p::-1::_iface::o_methods Ready {arglist {}}
-proc ::p::-1::Ready {_ID_} {
- return [expr {![set ::p::[lindex [dict get $_ID_ i this] 0 0]::_iface::o_open]}]
-}
-
-
-
-dict set ::p::-1::_iface::o_methods Destroy {arglist {{force 1}}}
-
-#'force' 1 indicates object command & variable will also be removed.
-#'force' 0 is used when the containing namespace is being destroyed anyway - so no need to destroy cmd & var.
-#this is necessary for versions of Tcl that have problems with 'unset' being called multiple times. (e.g Tcl 8.5a4)
-#
-proc ::p::-1::Destroy {_ID_ {force 1}} {
- #puts stdout "\t\tDestroy called with _ID_:$_ID_ force:$force caller:[info level 1]"
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
-
- if {$OID eq "null"} {
- puts stderr "warning - review code. Destroy called on object with null OID. _ID_:$_ID_"
- return
- }
-
- upvar #0 ::p::${OID}::_meta::map MAP
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
-
- #puts ">>>>>Explicit Destroy $cmd [clock format [clock seconds] -format %H:%M:%S] info-level-1'[info level 1]'<<<<<" ;flush stdout
-
- #explicit Destroy - remove traces
- #puts ">>TRACES: [trace info variable $cmd]"
- #foreach tinfo [trace info variable $cmd] {
- # trace remove variable $cmd {*}$tinfo
- #}
- #foreach tinfo [trace info command $cmd] {
- # trace remove command $cmd {*}$tinfo
- #}
-
-
- set _cmd [string map {::> ::} $cmd]
-
- #set ifaces [lindex $map 1]
- set iface_stacks [dict get $MAP interfaces level0]
- #set patterns [lindex $map 2]
- set pattern_stacks [dict get $MAP interfaces level1]
-
-
-
- set ifaces $iface_stacks
-
-
- set patterns $pattern_stacks
-
-
- #set i 0
- #foreach iflist $ifaces {
- # set IFID$i [lindex $iflist 0]
- # incr i
- #}
-
-
- set IFTOP [lindex $ifaces end]
-
- set DESTRUCTOR ::p::${IFTOP}::___system___destructor
- #may be a proc, or may be an alias
- if {[namespace which $DESTRUCTOR] ne ""} {
- set temp_ID_ [dict create i [dict create this [list [dict get $MAP invocantdata]]] context {}]
-
- if {[catch {$DESTRUCTOR $temp_ID_} prob]} {
- #!todo - ensure correct calling order of interfaces referencing the destructor proc
-
-
- #!todo - emit destructor errors somewhere - logger?
- #puts stderr "underlying proc already removed??? ---> $prob"
- #puts stderr "--------Destructor Error on interface $IFID0 of Object $OID-------------"
- #puts stderr $::errorInfo
- #puts stderr "---------------------"
- }
- }
-
-
- #remove ourself from each interfaces list of referencers
- #puts stderr "--- $ifaces"
-
- foreach var {ifaces patterns} {
-
- foreach i [set $var] {
-
- if {[string length $i]} {
- if {$i == 2} {
- #skip the >ifinfo interface which doesn't maintain a usedby list anyway.
- continue
- }
-
- if {[catch {
-
- upvar #0 ::p::${i}::_iface::o_usedby usedby
-
- array unset usedby i$OID
-
-
- #puts "\n***>>***"
- #puts "IFACE: $i usedby: $usedby"
- #puts "***>>***\n"
-
- #remove interface if no more referencers
- if {![array size usedby]} {
- #puts " **************** DESTROYING unused interface $i *****"
- #catch {namespace delete ::p::$i}
-
- #we happen to know where 'interface' object commands are kept:
-
- ::p::ifaces::>$i .. Destroy
-
- }
-
- } errMsg]} {
- #warning
- puts stderr "warning: error during destruction of object:$OID (removing usedby reference for interface $i) ([lindex [dict get $MAP invocantdata] 3]) \n $errMsg"
- }
- }
-
- }
-
- }
-
- set ns ::p::${OID}
- #puts "-- destroying objects below namespace:'$ns'"
- ::p::internals::DestroyObjectsBelowNamespace $ns
- #puts "--.destroyed objects below '$ns'"
-
-
- #set ns ::p::${OID}::_sub
- #call .. Destroy on each thing that looks like a pattern object anywhere below our 'user-area' namespace
- #( ::p::OBJECT::$OID )
- #puts "\n******** [clock format [clock seconds] -format %H:%M:%S] destroyingobjectsbelownamespace ns: $ns *****\n"
- #::p::internals::DestroyObjectsBelowNamespace $ns
-
- #same for _meta objects (e.g Methods,Properties collections)
- #set ns ::p::${OID}::_meta
- #::p::internals::DestroyObjectsBelowNamespace $ns
-
-
-
- #foreach obj [info commands ${ns}::>*] {
- # #Assume it's one of ours, and ask it to die.
- # catch {::p::meta::Destroy $obj}
- # #catch {$cmd .. Destroy}
- #}
- #just in case the user created subnamespaces.. kill objects there too.
- #foreach sub [namespace children $ns] {
- # ::p::internals::DestroyObjectsBelowNamespace $sub
- #}
-
-
- #!todo - fix. info vars on the namespace is not enough to detect references which were never set to a value!
- #use info commands ::p::${OID}::_ref::* to find all references - including variables never set
- #remove variable traces on REF vars
- #foreach rv [info vars ::p::${OID}::_ref::*] {
- # foreach tinfo [trace info variable $rv] {
- # #puts "-->removing traces on $rv: $tinfo"
- # trace remove variable $rv {*}$tinfo
- # }
- #}
-
- #!todo - write tests
- #refs create aliases and variables at the same place
- #- but variable may not exist if it was never set e.g if it was only used with info exists
- foreach rv [info commands ::p::${OID}::_ref::*] {
- foreach tinfo [trace info variable $rv] {
- #puts "-->removing traces on $rv: $tinfo"
- trace remove variable $rv {*}$tinfo
- }
- }
-
-
-
-
-
-
-
- #if {[catch {namespace delete $nsMeta} msg]} {
- # puts stderr "-----&&&&&&&&&&&&&& ERROR deleting NS $nsMeta : $msg "
- #} else {
- # #puts stderr "------ -- -- -- -- deleted $nsMeta "
- #}
-
-
- #!todo - remove
- #temp
- #catch {interp alias "" ::>$OID ""}
-
- if {$force} {
- #rename $cmd {}
-
- #removing the alias will remove the command - even if it's been renamed
- interp alias {} $alias {}
-
- #if {[catch {rename $_cmd {} } why]} {
- # #!todo - work out why some objects don't have matching command.
- # #puts stderr "\t rename $_cmd {} failed"
- #} else {
- # puts stderr "\t rename $_cmd {} SUCCEEDED!!!!!!!!!!"
- #}
-
- }
-
- set refns ::p::${OID}::_ref
- #puts "[clock format [clock seconds] -format %H:%M:%S] - tidying up namespace $refns"
- #puts "- children: [llength [namespace children $refns]]"
- #puts "- vars : [llength [info vars ${refns}::*]]"
- #puts "- commands: [llength [info commands ${refns}::*]]"
- #puts "- procs : [llength [info procs ${refns}::*]]"
- #puts "- aliases : [llength [lsearch -all -inline [interp aliases {}] ${refns}::*]]"
- #puts "- matching command: [llength [info commands ${refns}]]"
- #puts "[clock format [clock seconds] -format %H:%M:%S] - tidyup DONE $refns"
-
-
- #foreach v [info vars ${refns}::*] {
- # unset $v
- #}
- #foreach p [info procs ${refns}::*] {
- # rename $p {}
- #}
- #foreach a [lsearch -all -inline [interp aliases {}] ${refns}::*] {
- # interp alias {} $a {}
- #}
-
-
- #set ts1 [clock seconds]
- #puts "[clock format $ts1 -format %H:%M:%S] $cmd about to delete $refns."
- #puts "- children: [llength [namespace children $refns]]"
- #puts "- vars : [llength [info vars ${refns}::*]]"
-
- #puts "- commands: [llength [info commands ${refns}::*]]"
- #puts "- procs : [llength [info procs ${refns}::*]]"
- #puts "- aliases : [llength [lsearch -all -inline [interp aliases {}] ${refns}::*]]"
- #puts "- exact command: [info commands ${refns}]"
-
-
-
-
- #puts "--delete ::p::${OID}::_ref"
- if {[namespace exists ::p::${OID}::_ref]} {
- #could just catch.. but would rather know if there's some other weird reason the namespace can't be deleted.
- namespace delete ::p::${OID}::_ref::
- }
- set ts2 [clock seconds]
- #puts "[clock format $ts2 -format %H:%M:%S] $cmd deleted $refns. ELAPSED: [expr {$ts2 - $ts1}]"
-
-
- #delete namespace where instance variables reside
- #catch {namespace delete ::p::$OID}
- namespace delete ::p::$OID
-
- #puts "...... destroyed $cmd [clock format [clock seconds] -format %H:%M:%S] <<<<<" ;flush stdout
- return
-}
-
-
-interp alias {} ::p::-1::destructor {} ::p::-1::Destructor ;#for Define compatibility
-
-
-dict set ::p::-1::_iface::o_methods Destructor {arglist {args}}
-#!todo - destructor arguments? e.g to be able to mark for destruction on next sweep of some collector as opposed to immediate destruction?
-#install a Destructor on the invocant's open level1 interface.
-proc ::p::-1::Destructor {_ID_ args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- #lassign [lindex $map 0] OID alias itemCmd cmd
-
- set patterns [dict get $MAP interfaces level1]
-
- if {[llength $args] > 2} {
- error "too many arguments to 'Destructor' - expected at most 2 (arglist body)"
- }
-
- set existing_IID [lindex $patterns end] ;#!todo - get 'open' interface.
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- error "NOT TESTED"
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $patterns $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat [lreplace $patterns $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat [lreplace $patterns $posn $posn] $IID]
-
- #::p::predator::remap $invocant
- }
-
-
- set ::p::${IID}::_iface::o_destructor_body [lindex $args end]
-
- if {[llength $args] > 1} {
- #!todo - allow destructor args(?)
- set arglist [lindex $args 0]
- } else {
- set arglist [list]
- }
-
- set ::p::${IID}::_iface::o_destructor_args $arglist
-
- return
-}
-
-
-
-
-
-interp alias {} ::p::-1::method {} ::p::-1::PatternMethod ;#for Define compatibility (with snit)
-
-
-dict set ::p::-1::_iface::o_methods PatternMethod {arglist {method arglist body}}
-proc ::p::-1::PatternMethod {_ID_ method arglist body} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
- lassign [dict get $MAP invocantdata] OID alias default_method object_command _wrapped
-
- set patterns [dict get $MAP interfaces level1]
- set iid_top [lindex $patterns end] ;#!todo - get 'open' interface.
- set iface ::p::ifaces::>$iid_top
-
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- #no existing pattern - create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat $patterns $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- }
- set IID $iid_top
-
-
- namespace upvar ::p::${IID}::_iface o_methods o_methods o_definition o_definition o_varspace o_varspace o_varspaces o_varspaces
-
-
- # examine the existing command-chain
- set maxversion [::p::predator::method_chainhead $IID $method]
- set headid [expr {$maxversion + 1}]
- set THISNAME $method.$headid ;#first version will be $method.1
-
- set next [::p::predator::next_script $IID $method $THISNAME $_ID_]
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- #puts stdout "!!!>!>>>>>$THISNAME VarDecls: $varDecls"
- set body $varDecls\n[dict get $processed body]
- #puts stderr "\t object $OID runtime_vardecls in PatternMethod $method $varDecls"
- }
-
-
- set body [::p::predator::wrap_script_in_apply_object_namespace $o_varspace $body[set body {}] $arglist]
-
- #set body [string map [::list @this@ "\[lindex \${_ID_} 0 3]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata\] 3\]" @next@ $next] $body[set body {}]\n]
- #puts "\t\t--------------------"
- #puts "\n"
- #puts $body
- #puts "\n"
- #puts "\t\t--------------------"
- proc ::p::${IID}::_iface::$THISNAME [concat _ID_ $arglist] $body
-
-
-
- #pointer from method-name to head of the interface's command-chain
- interp alias {} ::p::${IID}::_iface::$method {} ::p::${IID}::_iface::$THISNAME
-
-
-
- if {$method in [dict keys $o_methods]} {
- #error "patternmethod '$method' already present in interface $IID"
- set msg "WARNING: patternmethod '$method' already exists on objectid $OID ($object_command). Replacing previous version. (no chaining support here yet...)"
- if {[string match "*@next@*" $body]} {
- append msg "\n EXTRA-WARNING: method contains @next@"
- }
-
- puts stdout $msg
- } else {
- dict set o_methods $method [list arglist $arglist]
- }
-
- #::p::-1::update_invocant_aliases $_ID_
- return
-}
-
-#MultiMethod
-#invocant_signature records the rolenames and aritys as a dispatch signature to support multimethods which act on any number of invocants
-# e.g1 $obj .. MultiMethod add {these 2} $arglist $body
-# e.g2 $obj .. MultiMethod add {these n} $arglist $body
-#
-# e.g3 $collidabletemplate .. MultiMethod collision {vehicles 2 cameras 0..n} $arglist $body
-#
-# for e.g3 - all vehicles & cameras involved would need to have the interface containing the method named 'collision', with the matching invocant_signature.
-# (it is possible for the object, or even the same interface to contain another method named 'collision' with a different signature)
-# !todo - review rules for when invocants participating in a multimethod with a particular signature, have different implementations (method from different interfaces)
-# - can we avoid the overhead of checking for this at dispatch-time, and simply use which ever implementation we first encounter?
-# - should we warn about or enforce a same-implementation rule for all multimethod conflicts found at the time an object-conglomeration is formed?
-# - should there be before and after hooks for all invocants involved in a multimethod so they can each add behaviour independent of the shared multimethod code?
-# (and how would we define the call order? - presumably as it appears in the conglomerate)
-# (or could that be done with a more general method-wrapping mechanism?)
-#...should multimethods use some sort of event mechanism, and/or message-passing system?
-#
-dict set ::p::-1::_iface::o_methods MultiMethod {arglist {method invocant_signature arglist body args}}
-proc ::p::-1::MultiMethod {_ID_ method invocant_signature arglist body args} {
- set invocants [dict get $_ID_ i]
-
- error "not implemented"
-}
-
-dict set ::p::-1::_iface::o_methods DefaultMethod {arglist {{methodname "noargsupplied--9e40ec8b-bc31-4400-98b8-d48ee23746c4"}}}
-# we could use . to indicate no methodname - as this is one of a few highly confusing names for a method (also for example .. , # -- )
-#we can create a method named "." by using the argprotect operator --
-# e.g >x .. Method -- . {args} $body
-#It can then be called like so: >x . .
-#This is not guaranteed to work and is not in the test suite
-#for now we'll just use a highly unlikely string to indicate no argument was supplied
-proc ::p::-1::DefaultMethod {_ID_ {methodname "noargsupplied--9e40ec8b-bc31-4400-98b8-d48ee23746c4"} } {
- set non_argument_magicstring "noargsupplied--9e40ec8b-bc31-4400-98b8-d48ee23746c4"
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias default_method object_command _wrapped
- if {$methodname eq $non_argument_magicstring} {
- return $default_method
- } else {
- set extracted_value [dict get $MAP invocantdata]
- lset extracted_value 2 $methodname
- dict set MAP invocantdata $extracted_value ;#write modified value back
- #update the object's command alias to match
- interp alias {} $alias {} ;#first we must delete it
- interp alias {} $alias {} ::p::internals::predator [list i [list this [list $extracted_value ] ] context {}]
-
- #! $object_command was initially created as the renamed alias - so we have to do it again
- rename $alias $object_command
- trace add command $object_command rename [list $object_command .. Rename]
- return $methodname
- }
-}
-
-dict set ::p::-1::_iface::o_methods PatternDefaultMethod {arglist {{methodname "noargsupplied--9e40ec8b-bc31-4400-98b8-d48ee23746c4"}}}
-proc ::p::-1::PatternDefaultMethod {_ID_ {methodname "noargsupplied--9e40ec8b-bc31-4400-98b8-d48ee23746c4"} } {
- set non_argument_magicstring "noargsupplied--9e40ec8b-bc31-4400-98b8-d48ee23746c4"
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set extracted_patterndata [dict get $MAP patterndata]
- set pattern_default_method [dict get $extracted_patterndata patterndefaultmethod]
- if {$methodname eq $non_argument_magicstring} {
- return $pattern_default_method
- } else {
- dict set extracted_patterndata patterndefaultmethod $methodname
- dict set MAP patterndata $extracted_patterndata
- return $methodname
- }
-}
-
-
-dict set ::p::-1::_iface::o_methods Method {arglist {method arglist bodydef args}}
-proc ::p::-1::Method {_ID_ method arglist bodydef args} {
- set invocants [dict get $_ID_ i]
-
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
-
- set invocant_signature [list] ;
- ;# we sort when calculating the sig.. so a different key order will produce the same signature - !todo - this is probably desirable but review anyway.
- foreach role [lsort [dict keys $invocants]] {
- lappend invocant_signature $role [llength [dict get $invocants $role]]
- }
- #note: it's expected that by far the most common 'invocant signature' will be {this 1} - which corresponds to a standard method dispatch on a single invocant object - the 'subject' (aka 'this')
-
-
-
- lassign [dict get $MAP invocantdata] OID alias default_method object_command
- set interfaces [dict get $MAP interfaces level0]
-
-
-
- #################################################################################
- if 0 {
- set iid_top [lindex $interfaces end] ;#!todo - get 'open' interface
- set prev_open [set ::p::${iid_top}::_iface::o_open]
-
- set iface ::p::ifaces::>$iid_top
-
- set f_new 0
- if {![string length $iid_top]} {
- set f_new 1
- } else {
- if {[$iface . isClosed]} {
- set f_new 1
- }
- }
- if {$f_new} {
- #create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat $interfaces $iid_top]
- dict set MAP interfaces $extracted_sub_dict
-
- }
- set IID $iid_top
-
- }
- #################################################################################
-
- set IID [::p::predator::get_possibly_new_open_interface $OID]
-
- #upvar 0 ::p::${IID}:: IFACE
-
- namespace upvar ::p::${IID}::_iface o_methods o_methods o_definition o_definition o_varspace o_varspace o_varspaces o_varspaces
-
-
- #Interface proc
- # examine the existing command-chain
- set maxversion [::p::predator::method_chainhead $IID $method]
- set headid [expr {$maxversion + 1}]
- set THISNAME $method.$headid ;#first version will be $method.1
-
- if {$method ni [dict keys $o_methods]} {
- dict set o_methods $method [list arglist $arglist]
- }
-
- #next_script will call to lower interface in iStack if we are $method.1
- set next [::p::predator::next_script $IID $method $THISNAME $_ID_] ;#last parameter is caller_ID_
- #puts ">!>>$THISNAME>>>>> next: '$next'<<<<<<"
-
-
- #implement
- #-----------------------------------
- set processed [dict create {*}[::p::predator::expand_var_statements $bodydef $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- set varDecls ""
- } else {
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- set body $varDecls\n[dict get $processed body]
- }
-
-
- set body [::p::predator::wrap_script_in_apply_object_namespace $o_varspace $body $arglist]
-
-
-
-
-
-
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
- #if {[string length $varDecls]} {
- # puts stdout "\t---------------------------------------------------------------"
- # puts stdout "\t----- efficiency warning - implicit var declarations used -----"
- # puts stdout "\t-------- $object_command .. Method $method $arglist ---------"
- # puts stdout "\t[string map [list \n \t\t\n] $body]"
- # puts stdout "\t--------------------------"
- #}
- #invocants are stored as a nested dict in the Invocant Data parameter (_ID_) under the key 'i', and then the invocant_role
- # while 'dict get $_ID_ i this' should always return a single invocant, all roles theoretically return a list of invocants fulfilling that position.
- #(as specified by the @ operator during object conglomeration)
- #set body [string map [::list @this@ "\[dict get \$_ID_ i this \]" @next@ $next] $body\n]
-
- #puts stdout "\t\t----------------------------"
- #puts stdout "$body"
- #puts stdout "\t\t----------------------------"
-
- proc ::p::${IID}::_iface::$THISNAME [concat _ID_ $arglist] $body
-
- #-----------------------------------
-
- #pointer from method-name to head of override-chain
- interp alias {} ::p::${IID}::_iface::$method {} ::p::${IID}::_iface::$THISNAME
-
-
- #point to the interface command only. The dispatcher will supply the invocant data
- #interp alias {} ::p::${OID}::$method {} ::p::${IID}::_iface::$method
- set argvals ""
- foreach argspec $arglist {
- if {[llength $argspec] == 2} {
- set a [lindex $argspec 0]
- } else {
- set a $argspec
- }
- if {$a eq "args"} {
- append argvals " \{*\}\$args"
- } else {
- append argvals " \$$a"
- }
- }
- set argvals [string trimleft $argvals]
- #this proc directly on the object is not *just* a forwarding proc
- # - it provides a context in which the 'uplevel 1' from the running interface proc runs
- #This (in 2018) is faster than a namespace alias forward to an interface proc which used apply to run in the dynamically calculated namespace (it seems the dynamic namespace stopped it from byte-compiling?)
-
- #we point to the method of the same name in the interface - which is an interp alias to the head of the implementation chain
-
- proc ::p::${OID}::$method [list _ID_ {*}$arglist] [subst {
- ::p::${IID}::_iface::$method \$_ID_ $argvals
- }]
-
-
- if 0 {
- if {[llength $argvals]} {
- proc ::p::${OID}::$method [list _ID_ {*}$arglist] [string map [list @ID@ [list $_ID_] @iid@ $IID @m@ $method @argl@ $arglist @argv@ $argvals] {
- apply {{_ID_ @argl@} {::p::@iid@::_iface::@m@ $_ID_ @argl@}} @ID@ @argv@
- }]
- } else {
-
- proc ::p::${OID}::$method [list _ID_ {*}$arglist] [string map [list @ID@ [list $_ID_] @iid@ $IID @m@ $method @argl@ $arglist] {
- apply [list {_ID_ @argl@} {::p::@iid@::_iface::@m@ $_ID_ @argl@} [namespace current]] @ID@
- }]
-
- }
- }
-
-
- #proc ::p::${OID}::$method [list _ID_ {*}$arglist] [subst {
- # ::p::${IID}::_iface::$method \$_ID_ $argvals
- #}]
-
- #todo - for o_varspaces
- #install ::p::${OID}::${varspace}::$method with interp alias from ::p::${OID}::$method
- #- this should work correctly with the 'uplevel 1' procs in the interfaces
-
-
- if {[string length $o_varspace]} {
- if {[string match "::*" $o_varspace]} {
- namespace eval $o_varspace {}
- } else {
- namespace eval ::p::${OID}::$o_varspace {}
- }
- }
-
-
- #if the metainfo collection exists, update it. Don't worry if nonexistant as it will be created if needed.
- set colMethods ::p::${OID}::_meta::>colMethods
-
- if {[namespace which $colMethods] ne ""} {
- if {![$colMethods . hasKey $method]} {
- $colMethods . add [::p::internals::predator $_ID_ . $method .] $method
- }
- }
-
- #::p::-1::update_invocant_aliases $_ID_
- return
- #::>pattern .. Create [::>pattern .. Namespace]::>method_???
- #return $method_object
-}
-
-
-dict set ::p::-1::_iface::o_methods V {arglist {{glob *}}}
-proc ::p::-1::V {_ID_ {glob *}} {
- set invocants [dict get $_ID_ i]
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
-
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
-
-
- set vlist [list]
- foreach IID $ifaces {
- dict for {vname vdef} [set ::p::${IID}::_iface::o_variables] {
- if {[string match $glob $vname]} {
- lappend vlist $vname
- }
- }
- }
-
-
- return $vlist
-}
-
-#experiment from http://wiki.tcl.tk/4884
-proc p::predator::pipeline {args} {
- set lambda {return -level 0}
- foreach arg $args {
- set lambda [list apply [dict get {
- toupper {{lambda input} {string toupper [{*}$lambda $input]}}
- tolower {{lambda input} {string tolower [{*}$lambda $input]}}
- totitle {{lambda input} {string totitle [{*}$lambda $input]}}
- prefix {{lambda pre input} {string cat $pre [{*}$lambda $input]}}
- suffix {{lambda suf input} {string cat [{*}$lambda $input] $suf}}
- } [lindex $arg 0]] $lambda[set lambda {}] {*}[lrange $arg 1 end]]
- }
- return $lambda
-}
-
-proc ::p::predator::get_apply_arg_0_oid {} {
- set apply_args [lrange [info level 0] 2 end]
- puts stderr ">>>>> apply_args:'$apply_args'<<<<"
- set invocant [lindex $apply_args 0]
- return [lindex [dict get $invocant i this] 0 0]
-}
-proc ::p::predator::get_oid {} {
- #puts stderr "---->> [info level 1] <<-----"
- set _ID_ [lindex [info level 1] 1] ;#something like ::p::17::_iface::method.1 {i {this { {16 ::p::16 item ::>thing {} } } }} arg1 arg2
- tailcall lindex [dict get $_ID_ i this] 0 0
-}
-
-#todo - make sure this is called for all script installations - e.g propertyread etc etc
-#Add tests to check code runs in correct namespace
-#review - how does 'Varspace' command affect this?
-proc ::p::predator::wrap_script_in_apply_object_namespace {varspace body arglist} {
- #use 'lindex $a 0' to make sure we only get the variable name. (arglist may have defaultvalues)
- set arglist_apply ""
- append arglist_apply "\$_ID_ "
- foreach a $arglist {
- if {$a eq "args"} {
- append arglist_apply "{*}\$args"
- } else {
- append arglist_apply "\$[lindex $a 0] "
- }
- }
- #!todo - allow fully qualified varspaces
- if {[string length $varspace]} {
- if {[string match ::* $varspace]} {
- return "tailcall apply \[list \[list _ID_ $arglist\] \{$body\} $varspace \] $arglist_apply"
- } else {
- #return "uplevel 1 \[list apply \[list \[list _ID_ $arglist\] \{$body\} ::p::@OID@::$varspace \] $arglist_apply \]\n"
- return "tailcall apply \[list \[list _ID_ $arglist\] \{$body\} ::p::@OID@::$varspace \] $arglist_apply"
- }
- } else {
- #return "uplevel 1 \[list apply \[list \[list _ID_ $arglist\] \{$body\} ::p::@OID@ \] $arglist_apply \]\n"
- #return "tailcall try \[list apply \[list \[list _ID_ $arglist\] \{$body\} ::p::@OID@ \] $arglist_apply \]"
-
- set script "tailcall apply \[list \{_ID_"
-
- if {[llength $arglist]} {
- append script " $arglist"
- }
- append script "\} \{"
- append script $body
- append script "\} ::p::@OID@\] "
- append script $arglist_apply
- #puts stderr "\n88888888888888888888888888\n\t$script\n"
- #puts stderr "\n77777777777777777777777777\n\ttailcall apply \[list \[list _ID_ $arglist\] \{$body\} ::p::@OID@ \] $arglist_apply"
- #return $script
-
-
- #-----------------------------------------------------------------------------
- # 2018 candidates
- #
- #return "tailcall apply \[list \[list _ID_ $arglist\] \{$body\} ::p::@OID@ \] $arglist_apply" ;#ok - but doesn't seem to be bytecompiled
- #return "tailcall apply \[list {_ID_ $arglist} {$body} ::p::@OID@ \] $arglist_apply" ;#ok - but doesn't seem to be bytecompiled
-
-
- #this has problems with @next@ arguments! (also script variables will possibly interfere with each other)
- #faster though.
- #return "uplevel 1 \{$body\}"
- return "uplevel 1 [list $body]"
- #-----------------------------------------------------------------------------
-
-
-
-
- #set script "apply \[list \[list _ID_ $arglist\] \{$body\}\] $arglist_apply"
- #return "uplevel 1 \{$script\}"
-
- #return "puts stderr --\[info locals\]-- ;apply \[list {_ID_ $arglist} {$body} ::p::\[p::predator::get_oid\] \] $arglist_apply" ;#fail
- #return "apply \[list {_ID_ $arglist} {$body} ::p::\[p::predator::get_oid\] \] $arglist_apply" ;#fail
-
-
-
- #return "tailcall apply { {_ID_ $arglist} {$body} ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\] } $arglist_apply" ;#wrong
-
- #return "tailcall apply \[list {_ID_ $arglist} {apply { {_ID_ $arglist} {$body}} $arglist_apply } ::p::@OID@ \] $arglist_apply" ;#wrong ns
-
-
- #experiment with different dispatch mechanism (interp alias with 'namespace inscope')
- #-----------
- #return "apply { {_ID_ $arglist} {$body}} $arglist_apply"
-
-
- #return "uplevel 1 \{$body\}" ;#do nothing
-
- #----------
-
- #return "tailcall namespace inscope ::p::@OID@ \{apply \{\{_ID_ $arglist\} \{$body\}\}\} $arglist_apply" ;#wrong! doesn't evaluate in the correct namespace (wrong _ID_ ??)
-
- #return "tailcall apply \{\{_ID_ $arglist\} \{namespace inscope ::p::@OID@ \{$body\}\} \} $arglist_apply" ;#wrong - _ID_ now not available in $body
-
- #return "tailcall apply \{\{ns _ID_ $arglist\} \{ apply \[list {_ID_ $arglist} \{$body\} \$ns \] $arglist_apply \} \} ::p::@OID@ $arglist_apply" ;#no quicker
-
- #return "tailcall "
-
-
- }
-}
-
-
-#Handle 'var' and 'varspace' declarations in method/constructor/destructor/propertyread etc bodies.
-#expand 'var' statements inline in method bodies
-#The presence of a var statement in any code-branch will cause the processor to NOT insert the implicit default var statements.
-#
-#concept of 'varspace' to allow separation and/or sharing of contexts for cooperating interfaces
-#WARNING: within methods etc, varspace statements affect all following var statements.. i.e varspace not affected by runtime code-branches!
-# e.g if 1 {varspace x} else {varspace y} will always leave 'varspace y' in effect for following statements.
-#Think of var & varspace statments as a form of compile-time 'macro'
-#
-#caters for 2-element lists as arguments to var statement to allow 'aliasing'
-#e.g var o_thing {o_data mydata}
-# this will upvar o_thing as o_thing & o_data as mydata
-#
-proc ::p::predator::expand_var_statements {rawbody {varspace ""}} {
- set body {}
-
- #keep count of any explicit var statments per varspace in 'numDeclared' array
- # don't initialise numDeclared. We use numDeclared keys to see which varspaces have var statements.
-
- #default varspace is ""
- #varspace should only have leading :: if it is an absolute namespace path.
-
-
- foreach ln [split $rawbody \n] {
- set trimline [string trim $ln]
-
- if {$trimline eq "var"} {
- #plain var statement alone indicates we don't have any explicit declarations in this branch
- # and we don't want implicit declarations for the current varspace either.
- #!todo - implement test
-
- incr numDeclared($varspace)
-
- #may be further var statements e.g - in other code branches
- #return [list body $rawbody varspaces_with_explicit_vars 1]
- } elseif {([string range $trimline 0 2] eq "var") && ([string is space [string index $trimline 3]])} {
-
- #append body " upvar #0 "
- #append body " namespace upvar ::p::\[lindex \$_ID_ 0 0 \]${varspace} "
- #append body " namespace upvar ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]${varspace} "
-
- if {$varspace eq ""} {
- append body " namespace upvar ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\] "
- } else {
- if {[string match "::*" $varspace]} {
- append body " namespace upvar $varspace "
- } else {
- append body " namespace upvar ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::${varspace} "
- }
- }
-
- #any whitespace before or betw var names doesn't matter - about to use as list.
- foreach varspec [string range $trimline 4 end] {
- lassign [concat $varspec $varspec] var alias ;#var == alias if varspec only 1 element.
- ##append body "::p::\[lindex \$_ID_ 0 0 \]::${varspace}$var $alias "
- #append body "::p::\[lindex \$_ID_ 0 0 \]${varspace}$var $alias "
-
- append body "$var $alias "
-
- }
- append body \n
-
- incr numDeclared($varspace)
- } elseif {([string range $trimline 0 7] eq "varspace") && ([string is space -strict [string index $trimline 8]])} {
- #2021 REVIEW - why do we even need 'varspace x' commands in bodies? - just use 'namespace eval x' ???
- #it is assumed there is a single word following the 'varspace' keyword.
- set varspace [string trim [string range $trimline 9 end]]
-
- if {$varspace in [list {{}} {""}]} {
- set varspace ""
- }
- if {[string length $varspace]} {
- #set varspace ::${varspace}::
- #no need to initialize numDeclared($varspace) incr will work anyway.
- #if {![info exists numDeclared($varspace)]} {
- # set numDeclared($varspace) 0
- #}
-
- if {[string match "::*" $varspace]} {
- append body "namespace eval $varspace {} \n"
- } else {
- append body "namespace eval ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::$varspace {} \n"
- }
-
- #puts "!!!! here~! namespace eval ::p::\[lindex \$_ID_ 0 0\]$varspace {} "
- #append body "namespace eval ::p::\[lindex \$_ID_ 0 0\]$varspace {} \n"
- #append body "namespace eval ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]$varspace {} \n"
-
- #append body "puts \"varspace: created ns ::p::\[lindex \$_ID_ 0 0\]$varspace \"\n"
- }
- #!review - why? why do we need the magic 'default' name instead of just using the empty string?
- #if varspace argument was empty string - leave it alone
- } else {
- append body $ln\n
- }
- }
-
-
-
- set varspaces [array names numDeclared]
- return [list body $body varspaces_with_explicit_vars $varspaces]
-}
-
-
-
-
-#Interface Variables
-dict set ::p::-1::_iface::o_methods IV {arglist {{glob *}}}
-proc ::p::-1::IV {_ID_ {glob *}} {
- set invocants [dict get $_ID_ i]
-
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
-
- #!todo - test
- #return [dict keys ::p::${OID}::_iface::o_variables $glob]
-
- set members [list]
- foreach vname [dict keys [set ::p::${OID}::_iface::o_variables]] {
- if {[string match $glob $vname]} {
- lappend members $vname
- }
- }
- return $members
-}
-
-
-dict set ::p::-1::_iface::o_methods Methods {arglist {{idx ""}}}
-proc ::p::-1::Methods {_ID_ {idx ""}} {
- set invocants [dict get $_ID_ i]
- set this_invocant [lindex [dict get $invocants this] 0]
- lassign $this_invocant OID _etc
- #set map [dict get $this_info map]
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
- set col ::p::${OID}::_meta::>colMethods
-
- if {[namespace which $col] eq ""} {
- patternlib::>collection .. Create $col
- foreach IID $ifaces {
- foreach m [dict keys [set ::p::${IID}::_iface::o_methods]] {
- if {![$col . hasIndex $m]} {
- #todo - create some sort of lazy-evaluating method object?
- #set arglist [dict get [set ::p::${IID}::iface::o_methods] $m arglist]
- $col . add [::p::internals::predator $_ID_ . $m .] $m
- }
- }
- }
- }
-
- if {[string length $idx]} {
- return [$col . item $idx]
- } else {
- return $col
- }
-}
-
-dict set ::p::-1::_iface::o_methods M {arglist {}}
-proc ::p::-1::M {_ID_} {
- set invocants [dict get $_ID_ i]
- set this_invocant [lindex [dict get $invocants this] 0]
- lassign $this_invocant OID _etc
- #set map [dict get $this_info map]
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
-
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
- set members [list]
- foreach IID $ifaces {
- foreach m [dict keys [set ::p::${IID}::_iface::o_methods]] {
- lappend members $m
- }
- }
- return $members
-}
-
-
-#review
-#Interface Methods
-dict set ::p::-1::_iface::o_methods IM {arglist {{glob *}}}
-proc ::p::-1::IM {_ID_ {glob *}} {
- set invocants [dict get $_ID_ i]
- set this_invocant [lindex [dict get $invocants this] 0]
- lassign $this_invocant OID _etc
- #set map [dict get $this_info map]
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
-
-
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
- return [dict keys [set ::p::${OID}::_iface::o_methods] $glob]
-
-}
-
-
-
-dict set ::p::-1::_iface::o_methods InterfaceStacks {arglist {}}
-proc ::p::-1::InterfaceStacks {_ID_} {
- upvar #0 ::p::[lindex [dict get $_ID_ i this] 0 0]::_meta::map MAP
- return [dict get $MAP interfaces level0]
-}
-
-
-dict set ::p::-1::_iface::o_methods PatternStacks {arglist {}}
-proc ::p::-1::PatternStacks {_ID_} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- return [dict get $MAP interfaces level1]
-}
-
-
-#!todo fix. need to account for references which were never set to a value
-dict set ::p::-1::_iface::o_methods DeletePropertyReferences {arglist {}}
-proc ::p::-1::DeletePropertyReferences {_ID_} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- set cleared_references [list]
- set refvars [info vars ::p::${OID}::_ref::*]
- #unsetting vars will clear traces anyway - but we wish to avoid triggering the 'unset' traces - so we will explicitly remove all traces 1st.
- foreach rv $refvars {
- foreach tinfo [trace info variable $rv] {
- set ops {}; set cmd {}
- lassign $tinfo ops cmd
- trace remove variable $rv $ops $cmd
- }
- unset $rv
- lappend cleared_references $rv
- }
-
-
- return [list deleted_property_references $cleared_references]
-}
-
-dict set ::p::-1::_iface::o_methods DeleteMethodReferences {arglist {}}
-proc ::p::-1::DeleteMethodReferences {_ID_} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- set cleared_references [list]
-
- set iflist [dict get $MAP interfaces level0]
- set iflist_reverse [lreferse $iflist]
- #set iflist [dict get $MAP interfaces level0]
-
-
- set refcommands [info commands ::p::${OID}::_ref::*]
- foreach c $refcommands {
- set reftail [namespace tail $c]
- set field [lindex [split $c +] 0]
- set field_is_a_method 0
- foreach IFID $iflist_reverse {
- if {$field in [dict keys [set ::p::${IFID}::_iface::o_methods]]} {
- set field_is_a_method 1
- break
- }
- }
- if {$field_is_a_method} {
- #what if it's also a property?
- interp alias {} $c {}
- lappend cleared_references $c
- }
- }
-
-
- return [list deleted_method_references $cleared_references]
-}
-
-
-dict set ::p::-1::_iface::o_methods DeleteReferences {arglist {}}
-proc ::p::-1::DeleteReferences {_ID_} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias default_method this
-
- set result [dict create]
- dict set result {*}[$this .. DeletePropertyReferences]
- dict set result {*}[$this .. DeleteMethodReferences]
-
- return $result
-}
-
-##
-#Digest
-#
-#!todo - review
-# -> a variable containing empty string is the same as a non existant variable as far as digest is concerned.. is that bad? (probably!)
-#
-#!todo - write tests - check that digest changes when properties of contained objects change value
-#
-#!todo - include method/property/interfaces in digest calc, or provide a separate more comprehensive digest method?
-#
-dict set ::p::-1::_iface::o_methods Digest {arglist {args}}
-proc ::p::-1::Digest {_ID_ args} {
- set invocants [dict get $_ID_ i]
- # md5 c-version is faster than md4 tcl version... and more likely to be required in the interp for some other purpose anyway.
- #set this_invocant [lindex [dict get $invocants this] 0]
- #lassign $this_invocant OID _etc
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] _OID alias default_method this
-
-
- set interface_ids [dict get $MAP interfaces level0]
- set IFID0 [lindex $interface_ids end]
-
- set known_flags {-recursive -algorithm -a -indent}
- set defaults {-recursive 1 -algorithm md5 -indent ""}
- if {[dict exists $args -a] && ![dict exists $args -algorithm]} {
- dict set args -algorithm [dict get $args -a]
- }
-
- set opts [dict merge $defaults $args]
- foreach key [dict keys $opts] {
- if {$key ni $known_flags} {
- error "unknown option $key. Expected only: $known_flags"
- }
- }
-
-
- set known_algos {"" raw RAW none NONE md5 MD5 sha256 SHA256}
- if {[dict get $opts -algorithm] ni $known_algos} {
- error "call to Digest with unknown -algorithm [dict get $opts -algorithm]. Expected one of: $known_algos"
- }
- set algo [string tolower [dict get $opts -algorithm]]
-
- # append comma for each var so that all changes in adjacent vars detectable.
- # i.e set x 34; set y 5
- # must be distinguishable from:
- # set x 3; set y 45
-
- if {[dict get $opts -indent] ne ""} {
- set state ""
- set indent "[dict get $opts -indent]"
- } else {
- set state "---\n"
- set indent " "
- }
- append state "${indent}object_command: $this\n"
- set indent "${indent} "
-
- #append state "[lindex [interp alias {} $alias] 1]\n" ;#at the very least, include the object's interface state.
- append state "${indent}interfaces: [dict get $MAP interfaces]\n";#at the very least, include the object's interface state.
-
-
-
-
- #!todo - recurse into 'varspaces'
- set varspaces_found [list]
- append state "${indent}interfaces:\n"
- foreach IID $interface_ids {
- append state "${indent} - interface: $IID\n"
- namespace upvar ::p::${IID}::_iface o_varspace local_o_varspace o_varspaces local_o_varspaces
- append state "${indent} varspaces:\n"
- foreach vs $local_o_varspaces {
- if {$vs ni $varspaces_found} {
- lappend varspaces_found $vs
- append state "${indent} - varspace: $vs\n"
- }
- }
- }
-
- append state "${indent}vars:\n"
- foreach var [info vars ::p::${OID}::*] {
- append state "${indent} - [namespace tail $var] : \""
- if {[catch {append state "[set $var]"}]} {
- append state "[array get $var]"
- }
- append state "\"\n"
- }
-
- if {[dict get $opts -recursive]} {
- append state "${indent}sub-objects:\n"
- set subargs $args
- dict set subargs -indent "$indent "
- foreach obj [info commands ::p::${OID}::>*] {
- append state "[$obj .. Digest {*}$subargs]\n"
- }
-
- append state "${indent}sub-namespaces:\n"
- set subargs $args
- dict set subargs -indent "$indent "
- foreach ns [namespace children ::p::${OID}] {
- append state "${indent} - namespace: $ns\n"
- foreach obj [info commands ${ns}::>*] {
- append state "[$obj .. Digest {*}$subargs]\n"
- }
- }
- }
-
-
- if {$algo in {"" raw none}} {
- return $state
- } else {
- if {$algo eq "md5"} {
- package require md5
- return [::md5::md5 -hex $state]
- } elseif {$algo eq "sha256"} {
- package require sha256
- return [::sha2::sha256 -hex $state]
- } elseif {$algo eq "blowfish"} {
- package require patterncipher
- patterncipher::>blowfish .. Create >b1
- set [>b1 . key .] 12341234
- >b1 . encrypt $state -final 1
- set result [>b1 . ciphertext]
- >b1 .. Destroy
-
- } elseif {$algo eq "blowfish-binary"} {
-
- } else {
- error "can't get here"
- }
-
- }
-}
-
-
-dict set ::p::-1::_iface::o_methods Variable {arglist {varname args}}
-proc ::p::-1::Variable {_ID_ varname args} {
- set invocants [dict get $_ID_ i]
-
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
-
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- #this interface itself is always a co-invocant
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set interfaces [dict get $MAP interfaces level0]
-
- #set existing_IID [lindex $map 1 0 end]
- set existing_IID [lindex $interfaces end]
-
- set prev_openstate [set ::p::${existing_IID}::_iface::o_open]
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #IID changed
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $interfaces $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat [lreplace $interfaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 0} [concat [lreplace $interfaces $posn $posn] $IID]
-
-
- #update original object command
- set ::p::${IID}::_iface::o_open 0
- } else {
- set ::p::${IID}::_iface::o_open $prev_openstate
- }
-
- set varspace [set ::p::${IID}::_iface::o_varspace] ;#varspace at the time this Variable was added (may differ from default for interface)
-
- if {[llength $args]} {
- #!assume var not already present on interface - it is an error to define twice (?)
- #lappend ::p::${IID}::_iface::o_variables [list $varname [lindex $args 0]]
- dict set ::p::${IID}::_iface::o_variables $varname [list default [lindex $args 0] varspace $varspace]
-
-
- #Implement if there is a default
- #!todo - correct behaviour when overlaying on existing object with existing var of this name?
- #if {[string length $varspace]} {
- # set ::p::${OID}::${varspace}::$varname [lindex $args 0]
- #} else {
- set ::p::${OID}::$varname [lindex $args 0]
- #}
- } else {
- #lappend ::p::${IID}::_iface::o_variables [list $varname]
- dict set ::p::${IID}::_iface::o_variables $varname [list varspace $varspace]
- }
-
- #varspace '_iface'
-
- return
-}
-
-
-#interp alias {} ::p::-1::variable {} ::p::-1::PatternVariable ;#for Define compatibility
-
-dict set ::p::-1::_iface::o_methods PatternVariable {arglist {varname args}}
-proc ::p::-1::PatternVariable {_ID_ varname args} {
- set invocants [dict get $_ID_ i]
-
- #set invocant_alias [lindex [dict get $invocants this] 0]
- #set invocant [lindex [interp alias {} $invocant_alias] 1]
- ##this interface itself is always a co-invocant
- #lassign [lindex $invocant 0 ] OID alias itemCmd cmd
-
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
-
-
- set patterns [dict get $MAP interfaces level1]
- set iid_top [lindex $patterns end] ;#!todo - get 'open' interface.
- set iface ::p::ifaces::>$iid_top
-
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- #no existing pattern - create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat $patterns $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat $patterns $iid_top]
- }
- set IID $iid_top
-
- set varspace [set ::p::${IID}::_iface::o_varspace] ;#record varspace against each variable, because default varspace for interface can be modified.
-
-
- if {[llength $args]} {
- #lappend ::p::${IID}::_iface::o_variables [list $varname [lindex $args 0]]
- dict set ::p::${IID}::_iface::o_variables $varname [list default [lindex $args 0] varspace $varspace]
- } else {
- dict set ::p::${IID}::_iface::o_variables $varname [list varspace $varspace]
- }
-
- return
-}
-
-dict set ::p::-1::_iface::o_methods Varspaces {arglist args}
-proc ::p::-1::Varspaces {_ID_ args} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- if {![llength $args]} {
- #query
- set iid_top [lindex [dict get $MAP interfaces level0] end]
- set iface ::p::ifaces::>$iid_top
- if {![string length $iid_top]} {
- error "Cannot query Varspaces because no top level interface on object:[lindex [dict get $MAP invocantdata] 3] "
- } elseif {[$iface . isClosed]} {
- error "Cannot query Varspaces because top level interface (id:$iid_top) is closed on object:[lindex [dict get $MAP invocantdata] 3] "
- }
- return [set ::p::${iid_top}::_iface::o_varspaces]
- }
- set IID [::p::predator::get_possibly_new_open_interface $OID]
- namespace upvar ::p::${IID}::_iface o_varspace o_varspace o_varspaces o_varspaces
-
- set varspaces $args
- foreach vs $varspaces {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- if {[string match ::* $vs} {
- namespace eval $vs {}
- } else {
- namespace eval ::p::${OID}::$vs {}
- }
- lappend o_varspaces $vs
- }
- }
- return $o_varspaces
-}
-
-#set or query Varspace. Error to query a closed interface, but if interface closed when writing, itwill create a new open interface
-dict set ::p::-1::_iface::o_methods Varspace {arglist args}
-# set the default varspace for the interface, so that new methods/properties refer to it.
-# varspace may be switched in between various additions of methods/properties so that different methods/properties are using different varspaces.
-proc ::p::-1::Varspace {_ID_ args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- if {![llength $args]} {
- #query
- set iid_top [lindex [dict get $MAP interfaces level0] end]
- set iface ::p::ifaces::>$iid_top
- if {![string length $iid_top]} {
- error "Cannot query Varspace because no top level interface on object:[lindex [dict get $MAP invocantdata] 3] "
- } elseif {[$iface . isClosed]} {
- error "Cannot query Varspace because top level interface (id:$iid_top) is closed on object:[lindex [dict get $MAP invocantdata] 3] "
- }
- return [set ::p::${iid_top}::_iface::o_varspace]
- }
- set varspace [lindex $args 0]
-
- #set interfaces [dict get $MAP interfaces level0]
- #set iid_top [lindex $interfaces end]
-
- set IID [::p::predator::get_possibly_new_open_interface $OID]
-
-
- #namespace upvar ::p::${IID}::_iface o_variables o_variables o_properties o_properties o_methods o_methods o_varspace o_varspace
- namespace upvar ::p::${IID}::_iface o_varspace o_varspace o_varspaces o_varspaces
-
- if {[string length $varspace]} {
- #ensure namespace exists !? do after list test?
- if {[string match ::* $varspace]} {
- namespace eval $varspace {}
- } else {
- namespace eval ::p::${OID}::$varspace {}
- }
- if {$varspace ni $o_varspaces} {
- lappend o_varspaces $varspace
- }
- }
- set o_varspace $varspace
-}
-
-
-proc ::p::predator::get_possibly_new_open_interface {OID} {
- #we need to re-upvar MAP rather than using a parameter - as we need to write back to it
- upvar #0 ::p::${OID}::_meta::map MAP
- set interfaces [dict get $MAP interfaces level0]
- set iid_top [lindex $interfaces end]
-
-
- set iface ::p::ifaces::>$iid_top
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- #no existing pattern - create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- #puts stderr ">>>>creating new interface $iid_top"
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat $interfaces $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- }
-
- return $iid_top
-}
-
-
-
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PatternVarspace {arglist {varspace args}}
-# set the default varspace for the interface, so that new methods/properties refer to it.
-# varspace may be switched in between various additions of methods/properties so that different methods/properties are using different varspaces.
-proc ::p::-1::PatternVarspace {_ID_ varspace args} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- set patterns [dict get $MAP interfaces level1]
- set iid_top [lindex $patterns end]
-
- set iface ::p::ifaces::>$iid_top
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- #no existing pattern - create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat $patterns $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- }
- set IID $iid_top
-
- namespace upvar ::p::${IID}::_iface o_varspace o_varspace o_varspaces o_varspaces
- if {[string length $varspace]} {
- if {$varspace ni $o_varspaces} {
- lappend o_varspaces $varspace
- }
- }
- #o_varspace is the currently active varspace
- set o_varspace $varspace
-
-}
-###################################################################################################################################################
-
-#get varspace and default from highest interface - return all interface ids which define it
-dict set ::p::-1::_iface::o_methods GetPropertyInfo {arglist {{propnamepattern *}}}
-proc ::p::-1::GetPropertyInfo {_ID_ {propnamepattern *}} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set interfaces [dict get $MAP interfaces level0]
-
- array set propinfo {}
- set found_property_names [list]
- #start at the lowest and work up (normal storage order of $interfaces)
- foreach iid $interfaces {
- set propinfodict [set ::p::${iid}::_iface::o_properties]
- set matching_propnames [dict keys $propinfodict $propnamepattern]
- foreach propname $matching_propnames {
- if {$propname ni $found_property_names} {
- lappend found_property_names $propname
- }
- lappend propinfo($propname,interfaces) $iid
- ;#These 2 values for this $propname are overwritten for each iid in the outer loop - we are only interested in the last one
- if {[dict exists $propinfodict $propname default]} {
- set propinfo($propname,default) [dict get $propinfodict $propname default]
- }
- set propinfo($propname,varspace) [dict get $propinfodict $propname varspace]
- }
- }
-
- set resultdict [dict create]
- foreach propname $found_property_names {
- set fields [list varspace $propinfo($propname,varspace)]
- if {[array exists propinfo($propname,default)]} {
- lappend fields default [set propinfo($propname,default)]
- }
- lappend fields interfaces $propinfo($propname,interfaces)
- dict set resultdict $propname $fields
- }
- return $resultdict
-}
-
-
-dict set ::p::-1::_iface::o_methods GetTopPattern {arglist args}
-proc ::p::-1::GetTopPattern {_ID_ args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- set interfaces [dict get $MAP interfaces level1]
- set iid_top [lindex $interfaces end]
- if {![string length $iid_top]} {
- lassign [dict get $MAP invocantdata] OID _alias _default_method object_command
- error "No installed level1 interfaces (patterns) for object $object_command"
- }
- return ::p::ifaces::>$iid_top
-}
-
-
-
-dict set ::p::-1::_iface::o_methods GetTopInterface {arglist args}
-proc ::p::-1::GetTopInterface {_ID_ args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- set iid_top [lindex [dict get $MAP interfaces level0] end]
- if {![string length $iid_top]} {
- lassign [dict get $MAP invocantdata] OID _alias _default_method object_command
- error "No installed level0 interfaces for object $object_command"
- }
- return ::p::ifaces::>$iid_top
-}
-
-
-dict set ::p::-1::_iface::o_methods GetExpandableInterface {arglist args}
-proc ::p::-1::GetExpandableInterface {_ID_ args} {
-
-}
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods Property {arglist {property args}}
-proc ::p::-1::Property {_ID_ property args} {
- #puts stderr "::p::-1::Property called with _ID_: '$_ID_' property:$property args:$args"
- #set invocants [dict get $_ID_ i]
- #set invocant_roles [dict keys $invocants]
- if {[llength $args] > 1} {
- error ".. Property expects 1 or 2 arguments only. (>object .. Property propertyname ?default?)"
- }
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- set interfaces [dict get $MAP interfaces level0]
- set iid_top [lindex $interfaces end]
-
- set prev_openstate [set ::p::${iid_top}::_iface::o_open]
-
- set iface ::p::ifaces::>$iid_top
-
-
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- #create a new interface
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat $interfaces $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- }
- set IID $iid_top
-
-
- namespace upvar ::p::${IID}::_iface o_variables o_variables o_properties o_properties o_methods o_methods o_varspace o_varspace
-
-
- set maxversion [::p::predator::method_chainhead $IID (GET)$property]
- set headid [expr {$maxversion + 1}]
- set THISNAME (GET)$property.$headid ;#first version will be (GET)$property.1
-
-
- if {$headid == 1} {
- #implementation
- #interp alias {} ::p::${IID}::_iface::(GET)$property.1 {} ::p::predator::getprop $property
-
- #if {$o_varspace eq ""} {
- # set ns ::p::${OID}
- #} else {
- # if {[string match "::*" $o_varspace]} {
- # set ns $o_varspace
- # } else {
- # set ns ::p::${OID}::$o_varspace
- # }
- #}
- #proc ::p::${IID}::_iface::(GET)$property.1 {_ID_ args} [string map [list %prop% $property %varspace% $o_varspace %ns% $ns] [info body ::p::predator::getprop_template_immediate]]
-
- proc ::p::${IID}::_iface::(GET)$property.1 {_ID_ args} [string map [list %prop% $property %varspace% $o_varspace ] [info body ::p::predator::getprop_template]]
-
-
- #interp alias {} ::p::${IID}::_iface::(SET)$property.1 {} ::p::predator::setprop $property
- proc ::p::${IID}::_iface::(SET)$property.1 {_ID_ args} [string map [list %prop% $property %varspace% $o_varspace] [info body ::p::predator::setprop_template]]
-
-
- #chainhead pointers
- interp alias {} ::p::${IID}::_iface::(GET)$property {} ::p::${IID}::_iface::(GET)$property.1
- interp alias {} ::p::${IID}::_iface::(SET)$property {} ::p::${IID}::_iface::(SET)$property.1
-
-
- }
-
- if {($property ni [dict keys $o_methods])} {
- interp alias {} ::p::${IID}::_iface::$property {} ::p::${IID}::_iface::(GET)$property
- }
-
-
-
- #installation on object
-
- #namespace eval ::p::${OID} [list namespace export $property]
-
-
-
- #obsolete?
- #if {$property ni [P $_ID_]} {
- #only link objects (GET)/(SET) for this property if property not present on any of our other interfaces
- #interp alias {} ::p::${OID}::(GET)$property {} ::p::${IID}::_iface::(GET)$property $invocant
- #interp alias {} ::p::${OID}::(SET)$property {} ::p::${IID}::_iface::(SET)$property $invocant
- #}
-
- #link main (GET)/(SET) to this interface
- interp alias {} ::p::${OID}::(GET)$property {} ::p::${IID}::_iface::(GET)$property
- interp alias {} ::p::${OID}::(SET)$property {} ::p::${IID}::_iface::(SET)$property
-
- #Only install property if no method of same name already installed here.
- #(Method takes precedence over property because property always accessible via 'set' reference)
- #convenience pointer to chainhead pointer.
- if {$property ni [M $_ID_]} {
- interp alias {} ::p::${OID}::$property {} ::p::${IID}::_iface::(GET)$property
- } else {
- #property with same name as method - we need to make sure the refMisuse_traceHandler is fixed
-
-
- }
-
-
- set varspace [set ::p::${IID}::_iface::o_varspace]
-
-
-
- #Install the matching Variable
- #!todo - which should take preference if Variable also given a default?
- #if {[set posn [lsearch -index 0 $o_variables o_$property]] >= 0} {
- # set o_variables [lreplace $o_variables $posn $posn o_$property]
- #} else {
- # lappend o_variables [list o_$property]
- #}
- dict set o_variables o_$property [list varspace $varspace]
-
-
-
-
- if {[llength $args]} {
- #should store default once only!
- #set IFINFO(v,default,o_$property) $default
-
- set default [lindex $args end]
-
- dict set o_properties $property [list default $default varspace $varspace]
-
- #if {[set posn [lsearch -index 0 $o_properties $property]] >= 0} {
- # set o_properties [lreplace $o_properties $posn $posn [list $property $default]]
- #} else {
- # lappend o_properties [list $property $default]
- #}
-
- if {$varspace eq ""} {
- set ns ::p::${OID}
- } else {
- if {[string match "::*" $varspace]} {
- set ns $varspace
- } else {
- set ns ::p::${OID}::$o_varspace
- }
- }
-
- set ${ns}::o_$property $default
- #set ::p::${OID}::o_$property $default
- } else {
-
- #if {[set posn [lsearch -index 0 $o_properties $property]] >= 0} {
- # set o_properties [lreplace $o_properties $posn $posn [list $property]]
- #} else {
- # lappend o_properties [list $property]
- #}
- dict set o_properties $property [list varspace $varspace]
-
-
- #variable ::p::${OID}::o_$property
- }
-
-
-
-
-
- #if the metainfo collection exists, update it. Don't worry if nonexistant as it will be created if needed.
- #!todo - mark interface dirty (not ready?) instead? - would need all colProperties methods to respect dirty flag & synchronize as needed. (object filter?)
- #catch {::p::OBJECT::${OID}::colProperties add [::p::internals::predator $invocant . $property .] $property}
-
- set colProperties ::p::${OID}::_meta::>colProperties
- if {[namespace which $colProperties] ne ""} {
- if {![$colProperties . hasKey $property]} {
- $colProperties . add [::p::internals::predator $_ID_ . $property .] $property
- }
- }
-
- return
-}
-###################################################################################################################################################
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-interp alias {} ::p::-1::option {} ::p::-1::PatternProperty ;#for Define compatibility
-dict set ::p::-1::_iface::o_methods PatternProperty {arglist {property args}}
-proc ::p::-1::PatternProperty {_ID_ property args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- set patterns [dict get $MAP interfaces level1]
- set iid_top [lindex $patterns end]
-
- set iface ::p::ifaces::>$iid_top
-
- if {(![string length $iid_top]) || ([$iface . isClosed])} {
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat $patterns $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat $patterns $iid_top]
- }
- set IID $iid_top
-
- namespace upvar ::p::${IID}::_iface o_properties o_properties o_variables o_variables o_varspace o_varspace
-
-
- set maxversion [::p::predator::method_chainhead $IID (GET)$property]
- set headid [expr {$maxversion + 1}]
- set THISNAME (GET)$property.$headid ;#first version will be (GET)$property.1
-
-
-
- if {$headid == 1} {
- #implementation
- #interp alias {} ::p::${IID}::_iface::(GET)$property.1 {} ::p::predator::getprop $property
- proc ::p::${IID}::_iface::(GET)$property.1 {_ID_ args} [string map [list %prop% $property %varspace% $o_varspace] [info body ::p::predator::getprop_template]]
- #interp alias {} ::p::${IID}::_iface::(SET)$property.1 {} ::p::predator::setprop $property
- proc ::p::${IID}::_iface::(SET)$property.1 {_ID_ args} [string map [list %prop% $property %varspace% $o_varspace] [info body ::p::predator::setprop_template]]
-
-
- #chainhead pointers
- interp alias {} ::p::${IID}::_iface::(GET)$property {} ::p::${IID}::_iface::(GET)$property.1
- interp alias {} ::p::${IID}::_iface::(SET)$property {} ::p::${IID}::_iface::(SET)$property.1
-
- }
-
- if {($property ni [dict keys [set ::p::${IID}::_iface::o_methods]])} {
- interp alias {} ::p::${IID}::_iface::$property {} ::p::${IID}::_iface::(GET)$property
- }
-
- set varspace [set ::p::${IID}::_iface::o_varspace]
-
- #Install the matching Variable
- #!todo - which should take preference if Variable also given a default?
- #if {[set posn [lsearch -index 0 $o_variables o_$property]] >= 0} {
- # set o_variables [lreplace $o_variables $posn $posn o_$property]
- #} else {
- # lappend o_variables [list o_$property]
- #}
- dict set o_variables o_$property [list varspace $varspace]
-
- set argc [llength $args]
-
- if {$argc} {
- if {$argc == 1} {
- set default [lindex $args 0]
- dict set o_properties $property [list default $default varspace $varspace]
- } else {
- #if more than one arg - treat as a dict of options.
- if {[dict exists $args -default]} {
- set default [dict get $args -default]
- dict set o_properties $property [list default $default varspace $varspace]
- } else {
- #no default value
- dict set o_properties $property [list varspace $varspace]
- }
- }
- #! only set default for property... not underlying variable.
- #lappend ::p::${IID}::_iface::o_variables [list o_$property [lindex $args 0]]
- } else {
- dict set o_properties $property [list varspace $varspace]
- }
- return
-}
-###################################################################################################################################################
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PatternPropertyRead {arglist {property args}}
-proc ::p::-1::PatternPropertyRead {_ID_ property args} {
- set invocants [dict get $_ID_ i]
-
- set this_invocant [lindex [dict get $_ID_ i this] 0] ;#assume only one 'this'
- set OID [lindex $this_invocant 0]
- #set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias defaut_command cmd
-
- set patterns [dict get $MAP interfaces level1]
- set existing_IID [lindex $patterns end]
-
- set idxlist [::list]
- if {[llength $args] == 1} {
- set body [lindex $args 0]
- } elseif {[llength $args] == 2} {
- lassign $args idxlist body
- } else {
- error "wrong # args: should be \"property body\" or \"property idxlist body\""
- }
-
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $patterns $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat [lreplace $patterns $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat [lreplace $patterns $posn $posn] $IID]
-
- } else {
- set prev_open [set ::p::${existing_IID}::_iface::o_open]
- set ::p::${IID}::_iface::o_open $prev_open
- }
-
- namespace upvar ::p::${IID}::_iface o_varspaces o_varspaces o_varspace o_varspace
-
- set maxversion [::p::predator::method_chainhead $IID (GET)$property]
- set headid [expr {$maxversion + 1}]
- if {$headid == 1} {
- set headid 2 ;#reserve 1 for the getprop of the underlying property
- }
-
- set THISNAME (GET)$property.$headid ;#first version will be (GET)$property.1
- set next [::p::predator::next_script $IID (GET)$property $THISNAME $_ID_] ;#last parameter is caller_ID_
-
-
- #implement
- #-----------------------------------
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
-
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- set body $varDecls[dict get $processed body]
- }
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
-
- #implementation
- if {![llength $idxlist]} {
- proc ::p::${IID}::_iface::(GET)$property.$headid {_ID_ args} $body
- } else {
- #what are we trying to achieve here? ..
- proc ::p::${IID}::_iface::(GET)$property.$headid [linsert $idxlist 0 _ID_] $body
- }
-
-
- #-----------------------------------
-
-
- #adjust chain-head pointer to point to new head.
- interp alias {} ::p::${IID}::_iface::(GET)$property {} ::p::${IID}::_iface::(GET)$property.$headid
-
- return
-}
-###################################################################################################################################################
-
-
-
-
-
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PropertyRead {arglist {property args}}
-proc ::p::-1::PropertyRead {_ID_ property args} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
-
- #assert $OID ne "null" - dispatcher won't call PropertyRead on a non-object(?) (presumably the call would be to 'Method' instead)
- lassign [dict get $MAP invocantdata] OID alias default_command cmd
-
- set interfaces [dict get $MAP interfaces level0]
- set existing_IID [lindex $interfaces end]
-
-
- set idxlist [::list]
- if {[llength $args] == 1} {
- set body [lindex $args 0]
- } elseif {[llength $args] == 2} {
- lassign $args idxlist body
- } else {
- error "wrong # args: should be \"property body\" or \"property idxlist body\""
- }
-
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $interfaces $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat [lreplace $interfaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
-
- set ::p::${IID}::_iface::o_open 0
- } else {
- set prev_open [set ::p::${existing_IID}::_iface::o_open]
- set ::p::${IID}::_iface::o_open $prev_open
- }
- namespace upvar ::p::${IID}::_iface o_varspaces o_varspaces o_varspace o_varspace
-
- #array set ::p::${IID}:: [::list pr,body,$property $body pr,arg,$property $idxlist pr,name,$property $property pr,iface,$property $cmd]
-
-
- set maxversion [::p::predator::method_chainhead $IID (GET)$property]
- set headid [expr {$maxversion + 1}]
- if {$headid == 1} {
- set headid 2
- }
- set THISNAME (GET)$property.$headid ;#first version will be (GET)$property.2 - even if corresponding property is missing (we reserve $property.1 for the property itself)
-
- set next [::p::predator::next_script $IID (GET)$property $THISNAME $_ID_]
-
- #implement
- #-----------------------------------
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- set body $varDecls[dict get $processed body]
- }
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
- proc ::p::${IID}::_iface::$THISNAME [concat _ID_ $idxlist] $body
-
- #-----------------------------------
-
-
-
- #pointer from prop-name to head of override-chain
- interp alias {} ::p::${IID}::_iface::(GET)$property {} ::p::${IID}::_iface::(GET)$property.$headid
-
-
- interp alias {} ::p::${OID}::(GET)$property {} ::p::${IID}::_iface::(GET)$property ;#the reference traces will call this one - in case there is both a property and a method with this name.
- if {$property ni [M $_ID_]} {
- interp alias {} ::p::${OID}::$property {} ::p::${IID}::_iface::(GET)$property
- }
-}
-###################################################################################################################################################
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PropertyWrite {arglist {property argname body}}
-proc ::p::-1::PropertyWrite {_ID_ property argname body} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias default_command cmd
-
- set interfaces [dict get $MAP interfaces level0]
- set existing_IID [lindex $interfaces end] ;#!todo - get 'open' interface.
-
-
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $interfaces $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat [lreplace $interfaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 0} [concat [lreplace $interfaces $posn $posn] $IID]
-
- set ::p::${IID}::_iface::o_open 0
- } else {
- set prev_open [set ::p::${existing_IID}::_iface::o_open]
- set ::p::${IID}::_iface::o_open $prev_open
- }
- namespace upvar ::p::${IID}::_iface o_varspaces o_varspaces o_varspace o_varspace
-
- #pw short for propertywrite
- #array set ::p::${IID}:: [::list pw,body,$property $body pw,arg,$property $argname pw,name,$property $property pw,iface,$property $cmd]
- array set ::p::${IID}:: [::list pw,body,$property $body pw,arg,$property $argname pw,name,$property $property]
-
-
- set maxversion [::p::predator::method_chainhead $IID (SET)$property]
- set headid [expr {$maxversion + 1}]
-
- set THISNAME (SET)$property.$headid
-
- set next [::p::predator::next_script $IID (SET)$property $THISNAME $_ID_]
-
- #implement
- #-----------------------------------
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- set body $varDecls[dict get $processed body]
- }
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
-
- proc ::p::${IID}::_iface::$THISNAME [list _ID_ $argname] $body
-
- #-----------------------------------
-
-
-
- #pointer from method-name to head of override-chain
- interp alias {} ::p::${IID}::_iface::(SET)$property {} ::p::${IID}::_iface::(SET)$property.$headid
-}
-###################################################################################################################################################
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PatternPropertyWrite {arglist {property argname body}}
-proc ::p::-1::PatternPropertyWrite {_ID_ property argname body} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias default_command cmd
-
-
- set patterns [dict get $MAP interfaces level1]
- set existing_IID [lindex $patterns end] ;#!todo - get 'open' interface.
-
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set existing_ifaces [lindex $map 1 1]
- set posn [lsearch $existing_ifaces $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat [lreplace $existing_ifaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat [lreplace $existing_ifaces $posn $posn] $IID]
-
- #set ::p::${IID}::_iface::o_open 0
- } else {
- }
-
- #pw short for propertywrite
- array set ::p::${IID}:: [::list pw,body,$property $body pw,arg,$property $argname pw,name,$property $property pw,iface,$property $cmd]
-
-
-
-
- return
-
-}
-###################################################################################################################################################
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PropertyUnset {arglist {property arraykeypattern body}}
-proc ::p::-1::PropertyUnset {_ID_ property arraykeypattern body} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias default_command cmd
-
-
- set interfaces [dict get $MAP interfaces level0]
- set existing_IID [lindex $interfaces end] ;#!todo - choose 'open' interface to expand.
-
-
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $interfaces $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat [lreplace $interfaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- } else {
- set prev_open [set ::p::${existing_IID}::_iface::o_open]
- set ::p::${IID}::_iface::o_open $prev_open
- }
- namespace upvar ::p::${IID}::_iface o_varspaces o_varspaces o_varspace o_varspace o_propertyunset_handlers propertyunset_handlers
- #upvar ::p::${IID}::_iface::o_propertyunset_handlers propertyunset_handlers
- dict set propertyunset_handlers $property [list body $body arraykeypattern $arraykeypattern]
-
- set maxversion [::p::predator::method_chainhead $IID (UNSET)$property]
- set headid [expr {$maxversion + 1}]
-
- set THISNAME (UNSET)$property.$headid
-
- set next [::p::predator::next_script $IID (UNSET)$property $THISNAME $_ID_]
-
-
- set processed [dict create {*}[::p::predator::expand_var_statements $body $o_varspace]]
- if {[llength [dict get $processed varspaces_with_explicit_vars]]} {
- foreach vs [dict get $processed varspaces_with_explicit_vars] {
- if {[string length $vs] && ($vs ni $o_varspaces)} {
- lappend o_varspaces $vs
- }
- }
- set body [dict get $processed body]
- } else {
- set varDecls [::p::predator::runtime_vardecls] ;#dynamic vardecls can access vars from all interfaces of invocant object.
- set body $varDecls[dict get $processed body]
- }
- #set body [string map [::list @this@ "\[lindex \$_ID_ 0 3 \]" @next@ $next] $body\n]
- set body [string map [::list @OID@ "\[lindex \[dict get \$_ID_ i this\] 0 0\]" @this@ "\[lindex \[dict get \[set ::p::\[lindex \[dict get \$_ID_ i this\] 0 0\]::_meta::map\] invocantdata \] 3\]" @next@ $next] $body\n]
-
- #note $arraykeypattern actually contains the name of the argument
- if {[string trim $arraykeypattern] eq ""} {
- set arraykeypattern _dontcare_ ;#
- }
- proc ::p::${IID}::_iface::(UNSET)$property.$headid [list _ID_ $arraykeypattern] $body
-
- #-----------------------------------
-
-
- #pointer from method-name to head of override-chain
- interp alias {} ::p::${IID}::_iface::(UNSET)$property {} ::p::${IID}::_iface::(UNSET)$property.$headid
-
-}
-###################################################################################################################################################
-
-
-
-
-
-
-
-
-###################################################################################################################################################
-
-###################################################################################################################################################
-dict set ::p::-1::_iface::o_methods PatternPropertyUnset {arglist {property arraykeypattern body}}
-proc ::p::-1::PatternPropertyUnset {_ID_ property arraykeypattern body} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
-
- set patterns [dict get $MAP interfaces level1]
- set existing_IID [lindex $patterns end] ;#!todo - choose 'open' interface to expand.
-
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $patterns $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat [lreplace $patterns $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #set ::p::${IID}::_iface::o_open 0
- }
-
-
- upvar ::p::${IID}::_iface::o_propertyunset_handlers propertyunset_handlers
- dict set propertyunset_handlers $property [list body $body arraykeypattern $arraykeypattern]
-
- return
-}
-###################################################################################################################################################
-
-
-
-#lappend ::p::-1::_iface::o_methods Implements
-#!todo - some way to force overriding of any abstract (empty) methods from the source object
-#e.g leave interface open and raise an error when closing it if there are unoverridden methods?
-
-
-
-
-
-#implementation reuse - sugar for >object .. Clone >target
-dict set ::p::-1::_iface::o_methods Extends {arglist {pattern}}
-proc ::p::-1::Extends {_ID_ pattern} {
- if {!([string range [namespace tail $pattern] 0 0] eq ">")} {
- error "'Extends' expected a pattern object"
- }
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd object_command
-
-
- tailcall $pattern .. Clone $object_command
-
-}
-#implementation reuse - sugar for >pattern .. Create >target
-dict set ::p::-1::_iface::o_methods PatternExtends {arglist {pattern}}
-proc ::p::-1::PatternExtends {_ID_ pattern} {
- if {!([string range [namespace tail $pattern] 0 0] eq ">")} {
- error "'PatternExtends' expected a pattern object"
- }
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd object_command
-
-
- tailcall $pattern .. Create $object_command
-}
-
-
-dict set ::p::-1::_iface::o_methods Extend {arglist {{idx ""}}}
-proc ::p::-1::Extend {_ID_ {idx ""}} {
- puts stderr "Extend is DEPRECATED - use Expand instead"
- tailcall ::p::-1::Expand $_ID_ $idx
-}
-
-#set the topmost interface on the iStack to be 'open'
-dict set ::p::-1::_iface::o_methods Expand {arglist {{idx ""}}}
-proc ::p::-1::Expand {_ID_ {idx ""}} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set interfaces [dict get $MAP interfaces level0] ;#level 0 interfaces
- set iid_top [lindex $interfaces end]
- set iface ::p::ifaces::>$iid_top
-
- if {![string length $iid_top]} {
- #no existing interface - create a new one
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [list $iid_top]
- dict set MAP interfaces $extracted_sub_dict ;#write new interface into map
- $iface . open
- return $iid_top
- } else {
- if {[$iface . isOpen]} {
- #already open..
- #assume ready to expand.. shared or not!
- return $iid_top
- }
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
- if {[$iface . refCount] > 1} {
- if {$iid_top != [set IID [::p::internals::expand_interface $iid_top ]]} {
- #!warning! not exercised by test suites!
-
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${iid_top}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- #remove existing interface & add
- set posn [lsearch $interfaces $iid_top]
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat [lreplace $interfaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 0} [concat [lreplace $interfaces $posn $posn] $IID]
-
-
- set iid_top $IID
- set iface ::p::ifaces::>$iid_top
- }
- }
- }
-
- $iface . open
- return $iid_top
-}
-
-dict set ::p::-1::_iface::o_methods PatternExtend {arglist {{idx ""}}}
-proc ::p::-1::PatternExtend {_ID_ {idx ""}} {
- puts stderr "PatternExtend is DEPRECATED - use PatternExpand instead"
- tailcall ::p::-1::PatternExpand $_ID_ $idx
-}
-
-
-
-#set the topmost interface on the pStack to be 'open' if it's not shared
-# if shared - 'copylink' to new interface before opening for extension
-dict set ::p::-1::_iface::o_methods PatternExpand {arglist {{idx ""}}}
-proc ::p::-1::PatternExpand {_ID_ {idx ""}} {
- set OID [::p::obj_get_this_oid $_ID_]
- ::p::map $OID MAP
- #puts stderr "no tests written for PatternExpand "
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
- set ifaces [dict get $MAP interfaces level1] ;#level 1 interfaces
- set iid_top [lindex $ifaces end]
- set iface ::p::ifaces::>$iid_top
-
- if {![string length $iid_top]} {
- #no existing interface - create a new one
- set iid_top [expr {$::p::ID + 1}] ;#PREDICT the next object's id
- set iface [::p::>interface .. Create ::p::ifaces::>$iid_top $OID]
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [list $iid_top]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [list $iid_top]
- $iface . open
- return $iid_top
- } else {
- if {[$iface . isOpen]} {
- #already open..
- #assume ready to expand.. shared or not!
- return $iid_top
- }
-
- if {[$iface . refCount] > 1} {
- if {$iid_top != [set IID [::p::internals::expand_interface $iid_top]]} {
- #!WARNING! not exercised by test suite!
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${iid_top}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $ifaces $iid_top]
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat [lreplace $ifaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat [lreplace $ifaces $posn $posn] $IID]
-
- set iid_top $IID
- set iface ::p::ifaces::>$iid_top
- }
- }
- }
-
- $iface . open
- return $iid_top
-}
-
-
-
-
-
-dict set ::p::-1::_iface::o_methods Properties {arglist {{idx ""}}}
-proc ::p::-1::Properties {_ID_ {idx ""}} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
- set col ::p::${OID}::_meta::>colProperties
-
- if {[namespace which $col] eq ""} {
- patternlib::>collection .. Create $col
- foreach IID $ifaces {
- dict for {prop pdef} [set ::p::${IID}::_iface::o_properties] {
- if {![$col . hasIndex $prop]} {
- $col . add [::p::internals::predator $_ID_ . $prop .] $prop
- }
- }
- }
- }
-
- if {[string length $idx]} {
- return [$col . item $idx]
- } else {
- return $col
- }
-}
-
-dict set ::p::-1::_iface::o_methods P {arglist {}}
-proc ::p::-1::P {_ID_} {
- set invocants [dict get $_ID_ i]
- set this_invocant [lindex [dict get $invocants this] 0]
- lassign $this_invocant OID _etc
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set interfaces [dict get $MAP interfaces level0] ;#level 0 interfaces
-
- set members [list]
- foreach IID $interfaces {
- foreach prop [dict keys [set ::p::${IID}::_iface::o_properties]] {
- lappend members $prop
- }
- }
- return [lsort $members]
-
-}
-#Interface Properties
-dict set ::p::-1::_iface::o_methods IP {arglist {{glob *}}}
-proc ::p::-1::IP {_ID_ {glob *}} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set ifaces [dict get $MAP interfaces level0] ;#level 0 interfaces
- set members [list]
-
- foreach m [dict keys [set ::p::${OID}::_iface::o_properties]] {
- if {[string match $glob [lindex $m 0]]} {
- lappend members [lindex $m 0]
- }
- }
- return $members
-}
-
-
-#used by rename.test - theoretically should be on a separate interface!
-dict set ::p::-1::_iface::o_methods CheckInvocants {arglist {args}}
-proc ::p::-1::CheckInvocants {_ID_ args} {
- #check all invocants in the _ID_ are consistent with data stored in their MAP variable
- set status "ok" ;#default to optimistic assumption
- set problems [list]
-
- set invocant_dict [dict get $_ID_ i]
- set invocant_roles [dict keys $invocant_dict]
-
- foreach role $invocant_roles {
- set invocant_list [dict get $invocant_dict $role]
- foreach aliased_invocantdata $invocant_list {
- set OID [lindex $aliased_invocantdata 0]
- set map_invocantdata [dict get [set ::p::${OID}::_meta::map] invocantdata]
- #we use lrange to make sure the lists are in canonical form
- if {[lrange $map_invocantdata 0 end] ne [lrange $aliased_invocantdata 0 end]} {
- set status "not-ok"
- lappend problems [list type "invocant_data_mismatch" invocant_role $role oid $OID command_invocantdata $aliased_invocantdata map_invocantdata $map_invocantdata]
- }
- }
-
- }
-
-
- set result [dict create]
- dict set result status $status
- dict set result problems $problems
-
- return $result
-}
-
-
-#get or set t
-dict set ::p::-1::_iface::o_methods Namespace {arglist {args}}
-proc ::p::-1::Namespace {_ID_ args} {
- #set invocants [dict get $_ID_ i]
- #set this_invocant [lindex [dict get $invocants this] 0]
- #lassign $this_invocant OID this_info
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- set IID [lindex [dict get $MAP interfaces level0] end]
-
- namespace upvar ::p::${IID}::_iface o_varspace active_varspace
-
- if {[string length $active_varspace]} {
- set ns ::p::${OID}::$active_varspace
- } else {
- set ns ::p::${OID}
- }
-
- #!todo - review.. 'eval' & 'code' subcommands make it too easy to violate the object?
- # - should .. Namespace be usable at all from outside the object?
-
-
- if {[llength $args]} {
- #special case some of the namespace subcommands.
-
- #delete
- if {[string match "d*" [lindex $args 0]]} {
- error "Don't destroy an object's namespace like this. Use '>object .. Destroy' to remove an object."
- }
- #upvar,ensemble,which,code,origin,expor,import,forget
- if {[string range [lindex $args 0] 0 1] in [list "up" "en" "wh" "co" "or" "ex" "im" "fo"]} {
- return [namespace eval $ns [list namespace {*}$args]]
- }
- #current
- if {[string match "cu*" [lindex $args 0]]} {
- return $ns
- }
-
- #children,eval,exists,inscope,parent,qualifiers,tail
- return [namespace {*}[linsert $args 1 $ns]]
- } else {
- return $ns
- }
-}
-
-
-
-
-
-
-
-
-
-
-dict set ::p::-1::_iface::o_methods PatternUnknown {arglist {args}}
-proc ::p::-1::PatternUnknown {_ID_ args} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- set patterns [dict get $MAP interfaces level1]
- set existing_IID [lindex $patterns end] ;#!todo - choose 'open' interface to expand.
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $patterns $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level1 [concat [lreplace $patterns $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 1} [concat [lreplace $patterns $posn $posn] $IID]
- #::p::predator::remap $invocant
- }
-
- set handlermethod [lindex $args 0]
-
-
- if {[llength $args]} {
- set ::p::${IID}::_iface::o_unknown $handlermethod
- return
- } else {
- set ::p::${IID}::_iface::o_unknown $handlermethod
- }
-
-}
-
-
-
-dict set ::p::-1::_iface::o_methods Unknown {arglist {args}}
-proc ::p::-1::Unknown {_ID_ args} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- set interfaces [dict get $MAP interfaces level0]
- set existing_IID [lindex $interfaces end] ;#!todo - choose 'open' interface to expand.
-
- set prev_open [set ::p::${existing_IID}::_iface::o_open]
-
- if {$existing_IID != [set IID [::p::internals::expand_interface $existing_IID]]} {
- #remove ourself from the usedby list of the previous interface
- array unset ::p::${existing_IID}::_iface::o_usedby i$OID
- set ::p::${IID}::_iface::o_usedby(i$OID) $cmd
-
- set posn [lsearch $interfaces $existing_IID]
-
- set extracted_sub_dict [dict get $MAP interfaces]
- dict set extracted_sub_dict level0 [concat [lreplace $interfaces $posn $posn] $IID]
- dict set MAP interfaces $extracted_sub_dict
- #lset map {1 0} [concat [lreplace $interfaces $posn $posn] $IID]
-
- set ::p::${IID}::_iface::o_open 0
- } else {
- set ::p::${IID}::_iface::o_open $prev_open
- }
-
- set handlermethod [lindex $args 0]
-
- if {[llength $args]} {
- set ::p::${IID}::_iface::o_unknown $handlermethod
- #set ::p::${IID}::(unknown) $handlermethod
-
-
- #interp alias {} ::p::${IID}::_iface::(UNKNOWN) {} ::p::${OID}::$handlermethod
- interp alias {} ::p::${IID}::_iface::(UNKNOWN) {} ::p::${IID}::_iface::$handlermethod
- interp alias {} ::p::${OID}::(UNKNOWN) {} ::p::${OID}::$handlermethod
-
- #namespace eval ::p::${IID}::_iface [list namespace unknown $handlermethod]
- #namespace eval ::p::${OID} [list namespace unknown $handlermethod]
-
- return
- } else {
- set ::p::${IID}::_iface::o_unknown $handlermethod
- }
-
-}
-
-
-#useful on commandline - can just uparrow and add to it to become ' .. As varname' instead of editing start and end of commandline to make it 'set varname []'
-# should also work for non-object results
-dict set ::p::-1::_iface::o_methods As {arglist {varname}}
-proc ::p::-1::As {_ID_ varname} {
- set invocants [dict get $_ID_ i]
- #puts stdout "invocants: $invocants"
- #!todo - handle multiple invocants with other roles, not just 'this'
-
- set OID [lindex [dict get $_ID_ i this] 0 0]
- if {$OID ne "null"} {
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- tailcall set $varname $cmd
- } else {
- #puts stdout "info level 1 [info level 1]"
- set role_members [dict get $_ID_ i this]
- if {[llength $role_members] == 1} {
- set member [lindex $role_members 0]
- lassign $member _OID namespace default_method stackvalue _wrapped
- tailcall set $varname $stackvalue
- } else {
- #multiple invocants - return all results as a list
- set resultlist [list]
- foreach member $role_members {
- lassign $member _OID namespace default_method stackvalue _wrapped
- lappend resultlist $stackvalue
- }
- tailcall set $varname $resultlist
- }
- }
-}
-
-#!todo - AsFileStream ??
-dict set ::p::-1::_iface::o_methods AsFile {arglist {filename args}}
-proc ::p::-1::AsFile {_ID_ filename args} {
- dict set default -force 0
- dict set default -dumpmethod ".. Digest -algorithm raw" ;#how to serialize/persist an object
- set opts [dict merge $default $args]
- set force [dict get $opts -force]
- set dumpmethod [dict get $opts -dumpmethod]
-
-
- if {[file pathtype $filename] eq "relative"} {
- set filename [pwd]/$filename
- }
- set filedir [file dirname $filename]
- if {![sf::file_writable $filedir]} {
- error "(method AsFile) ERROR folder $filedir is not writable"
- }
- if {[file exists $filename]} {
- if {!$force} {
- error "(method AsFile) ERROR file $filename already exists. Use -force 1 to overwrite"
- }
- if {![sf::file_writable $filename]} {
- error "(method AsFile) ERROR file $filename is not writable - check permissions"
- }
- }
- set fd [open $filename w]
- fconfigure $fd -translation binary
-
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $_ID_ i this] 0 0]
- if {$OID ne "null"} {
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
- #tailcall set $varname $cmd
- set object_data [$cmd {*}$dumpmethod]
- puts -nonewline $fd $object_data
- close $fd
- return [list status 1 bytes [string length $object_data] filename $filename]
- } else {
- #puts stdout "info level 1 [info level 1]"
- set role_members [dict get $_ID_ i this]
- if {[llength $role_members] == 1} {
- set member [lindex $role_members 0]
- lassign $member _OID namespace default_method stackvalue _wrapped
- puts -nonewline $fd $stackvalue
- close $fd
- #tailcall set $varname $stackvalue
- return [list status 1 bytes [string length $stackvalue] filename $filename]
- } else {
- #multiple invocants - return all results as a list
- set resultlist [list]
- foreach member $role_members {
- lassign $member _OID namespace default_method stackvalue _wrapped
- lappend resultlist $stackvalue
- }
- puts -nonewline $fd $resultset
- close $fd
- return [list status 1 bytes [string length $resultset] filename $filename]
- #tailcall set $varname $resultlist
- }
- }
-
-}
-
-
-
-dict set ::p::-1::_iface::o_methods Object {arglist {}}
-proc ::p::-1::Object {_ID_} {
- set invocants [dict get $_ID_ i]
- set OID [lindex [dict get $invocants this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
- set result [string map [list ::> ::] $cmd]
- if {![catch {info level -1} prev_level]} {
- set called_by "(called by: $prev_level)"
- } else {
- set called_by "(called by: interp?)"
-
- }
-
- puts stdout "\n\nWARNING: '.. Object' calls are now obsolete. Please adjust your code. $called_by ( [info level 1])\n\n"
- puts stdout " (returning $result)"
-
- return $result
-}
-
-#todo: make equivalent to >pattern = cmdname, >pattern . x = cmdname , >pattern # apiname = cmdname
-dict set ::p::-1::_iface::o_methods MakeAlias {arglist {cmdname}}
-proc ::p::-1::MakeAlias {_ID_cmdname } {
- set OID [::p::obj_get_this_oid $_ID_]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias itemCmd cmd
-
- error "concept probably won't work - try making dispatcher understand trailing '= cmdname' "
-}
-dict set ::p::-1::_iface::o_methods ID {arglist {}}
-proc ::p::-1::ID {_ID_} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- return $OID
-}
-
-dict set ::p::-1::_iface::o_methods IFINFO {arglist {}}
-proc ::p::-1::IFINFO {_ID_} {
- puts stderr "--_ID_: $_ID_--"
- set OID [::p::obj_get_this_oid $_ID_]
- upvar #0 ::p::${OID}::_meta::map MAP
-
- puts stderr "-- MAP: $MAP--"
-
- set interfaces [dict get $MAP interfaces level0]
- set IFID [lindex $interfaces 0]
-
- if {![llength $interfaces]} {
- puts stderr "No interfaces present at level 0"
- } else {
- foreach IFID $interfaces {
- set iface ::p::ifaces::>$IFID
- puts stderr "$iface : [$iface --]"
- puts stderr "\tis open: [set ::p::${IFID}::_iface::o_open]"
- set variables [set ::p::${IFID}::_iface::o_variables]
- puts stderr "\tvariables: $variables"
- }
- }
-
-}
-
-
-
-
-dict set ::p::-1::_iface::o_methods INVOCANTDATA {arglist {}}
-proc ::p::-1::INVOCANTDATA {_ID_} {
- #same as a call to: >object ..
- return $_ID_
-}
-
-#obsolete?
-dict set ::p::-1::_iface::o_methods UPDATEDINVOCANTDATA {arglist {}}
-proc ::p::-1::UPDATEDINVOCANTDATA {_ID_} {
- set updated_ID_ $_ID_
- array set updated_roles [list]
-
- set invocants [dict get $_ID_ i]
- set invocant_roles [dict keys $invocants]
- foreach role $invocant_roles {
-
- set role_members [dict get $invocants $role]
- foreach member [dict get $invocants $role] {
- #each member is a 2-element list consisting of the OID and a dictionary
- #each member is a 5-element list
- #set OID [lindex $member 0]
- #set object_dict [lindex $member 1]
- lassign $member OID alias itemcmd cmd wrapped
-
- set MAP [set ::p::${OID}::_meta::map]
- #if {[dictutils::equal {apply {{key v1 v2} {expr {$v1 eq $v2}}}} $mapvalue [dict get $object_dict map]]} {}
-
- if {[dict get $MAP invocantdata] eq $member}
- #same - nothing to do
-
- } else {
- package require overtype
- puts stderr "---------------------------------------------------------"
- puts stderr "UPDATEDINVOCANTDATA WARNING: invocantdata in _ID_ not equal to invocantdata in _meta::map - returning updated version"
- set col1 [string repeat " " [expr {[string length [dict get $MAP invocantdata]] + 2}]]
- puts stderr "[overtype::left $col1 {_ID_ map value}]: $member"
- puts stderr "[overtype::left $col1 ::p::${OID}::_meta::map]: [dict get $MAP invocantdata]"
- puts stderr "---------------------------------------------------------"
- #take _meta::map version
- lappend updated_roles($role) [dict get $MAP invocantdata]
- }
-
- }
-
- #overwrite changed roles only
- foreach role [array names updated_roles] {
- dict set updated_ID_ i $role [set updated_roles($role)]
- }
-
- return $updated_ID_
-}
-
-
-
-dict set ::p::-1::_iface::o_methods INFO {arglist {}}
-proc ::p::-1::INFO {_ID_} {
- set result ""
- append result "_ID_: $_ID_\n"
-
- set invocants [dict get $_ID_ i]
- set invocant_roles [dict keys $invocants]
- append result "invocant roles: $invocant_roles\n"
- set total_invocants 0
- foreach key $invocant_roles {
- incr total_invocants [llength [dict get $invocants $key]]
- }
-
- append result "invocants: ($total_invocants invocant(s) in [llength $invocant_roles] role(s)) \n"
- foreach key $invocant_roles {
- append result "\t-------------------------------\n"
- append result "\trole: $key\n"
- set role_members [dict get $invocants $key] ;#usually the role 'this' will have 1 member - but roles can have any number of invocants
- append result "\t Raw data for this role: $role_members\n"
- append result "\t Number of invocants in this role: [llength $role_members]\n"
- foreach member $role_members {
- #set OID [lindex [dict get $invocants $key] 0 0]
- set OID [lindex $member 0]
- append result "\t\tOID: $OID\n"
- if {$OID ne "null"} {
- upvar #0 ::p::${OID}::_meta::map MAP
- append result "\t\tmap:\n"
- foreach key [dict keys $MAP] {
- append result "\t\t\t$key\n"
- append result "\t\t\t\t [dict get $MAP $key]\n"
- append result "\t\t\t----\n"
- }
- lassign [dict get $MAP invocantdata] _OID namespace default_method cmd _wrapped
- append result "\t\tNamespace: $namespace\n"
- append result "\t\tDefault method: $default_method\n"
- append result "\t\tCommand: $cmd\n"
- append result "\t\tCommand Alias: [::pattern::which_alias $cmd]\n"
- append result "\t\tLevel0 interfaces: [dict get $MAP interfaces level0]\n"
- append result "\t\tLevel1 interfaces: [dict get $MAP interfaces level1]\n"
- } else {
- lassign $member _OID namespace default_method stackvalue _wrapped
- append result "\t\t last item on the predator stack is a value not an object"
- append result "\t\t Value is: $stackvalue"
-
- }
- }
- append result "\n"
- append result "\t-------------------------------\n"
- }
-
-
-
- return $result
-}
-
-
-
-
-dict set ::p::-1::_iface::o_methods Rename {arglist {args}}
-proc ::p::-1::Rename {_ID_ args} {
- set OID [::p::obj_get_this_oid $_ID_]
- if {![llength $args]} {
- error "Rename expected \$newname argument"
- }
-
- #Rename operates only on the 'this' invocant? What if there is more than one 'this'? should we raise an error if there is anything other than a single invocant?
- upvar #0 ::p::${OID}::_meta::map MAP
-
-
-
- #puts ">>.>> Rename. _ID_: $_ID_"
-
- if {[catch {
-
- if {([llength $args] == 3) && [lindex $args 2] eq "rename"} {
-
- #appears to be a 'trace command rename' firing
- #puts "\t>>>> rename trace fired $MAP $args <<<"
-
- lassign $args oldcmd newcmd
- set extracted_invocantdata [dict get $MAP invocantdata]
- lset extracted_invocantdata 3 $newcmd
- dict set MAP invocantdata $extracted_invocantdata
-
-
- lassign $extracted_invocantdata _oid alias _default_method object_command _wrapped
-
- #Write the same info into the _ID_ value of the alias
- interp alias {} $alias {} ;#first we must delete it
- interp alias {} $alias {} ::p::internals::predator [list i [list this [list $extracted_invocantdata ] ] context {}]
-
-
-
- #! $object_command was initially created as the renamed alias - so we have to do it again
- uplevel 1 [list rename $alias $object_command]
- trace add command $object_command rename [list $object_command .. Rename]
-
- } elseif {[llength $args] == 1} {
- #let the rename trace fire and we will be called again to do the remap!
- uplevel 1 [list rename [lindex [dict get $MAP invocantdata] 3] [lindex $args 0]]
- } else {
- error "Rename expected \$newname argument ."
- }
-
- } errM]} {
- puts stderr "\t@@@@@@ rename error"
- set ruler "\t[string repeat - 80]"
- puts stderr $ruler
- puts stderr $errM
- puts stderr $ruler
-
- }
-
- return
-
-
-}
-
-proc ::p::obj_get_invocants {_ID_} {
- return [dict get $_ID_ i]
-}
-#The invocant role 'this' is special and should always have only one member.
-# dict get $_ID_ i XXX will always return a list of invocants that are playing role XXX
-proc ::p::obj_get_this_oid {_ID_} {
- return [lindex [dict get $_ID_ i this] 0 0]
-}
-proc ::p::obj_get_this_ns {_ID_} {
- return [lindex [dict get $_ID_ i this] 0 1]
-}
-
-proc ::p::obj_get_this_cmd {_ID_} {
- return [lindex [dict get $_ID_ i this] 0 3]
-}
-proc ::p::obj_get_this_data {_ID_} {
- lassign [dict get [set ::p::[lindex [dict get $_ID_ i this] 0 0]::_meta::map] invocantdata] OID ns _unknown cmd
- #set this_invocant_data {*}[dict get $_ID_ i this]
- return [list oid $OID ns $ns cmd $cmd]
-}
-proc ::p::map {OID varname} {
- tailcall upvar #0 ::p::${OID}::_meta::map $varname
-}
-
-
-
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/mime-1.7.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/mime-1.7.1.tm
deleted file mode 100644
index b4b0d61d..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/mime-1.7.1.tm
+++ /dev/null
@@ -1,3934 +0,0 @@
-# mime.tcl - MIME body parts
-#
-# (c) 1999-2000 Marshall T. Rose
-# (c) 2000 Brent Welch
-# (c) 2000 Sandeep Tamhankar
-# (c) 2000 Dan Kuchler
-# (c) 2000-2001 Eric Melski
-# (c) 2001 Jeff Hobbs
-# (c) 2001-2008 Andreas Kupries
-# (c) 2002-2003 David Welton
-# (c) 2003-2008 Pat Thoyts
-# (c) 2005 Benjamin Riefenstahl
-# (c) 2013-2021 Poor Yorick
-#
-#
-# See the file "license.terms" for information on usage and redistribution
-# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
-#
-# Influenced by Borenstein's/Rose's safe-tcl (circa 1993) and Darren New's
-# unpublished package of 1999.
-#
-
-# new string features and inline scan are used, requiring 8.3.
-package require Tcl 8.5
-
-package provide mime 1.7.1
-package require tcl::chan::memchan
-
-
-if {[catch {package require Trf 2.0}]} {
-
- # Fall-back to tcl-based procedures of base64 and quoted-printable
- # encoders
- ##
- # Warning!
- ##
- # These are a fragile emulations of the more general calling
- # sequence that appears to work with this code here.
- ##
- # The `__ignored__` arguments are expected to be `--` options on
- # the caller's side. (See the uses in `copymessageaux`,
- # `buildmessageaux`, `parsepart`, and `getbody`).
-
- package require base64 2.0
- set ::major [lindex [split [package require md5] .] 0]
-
- # Create these commands in the mime namespace so that they
- # won't collide with things at the global namespace level
-
- namespace eval ::mime {
- proc base64 {-mode what __ignored__ chunk} {
- return [base64::$what $chunk]
- }
- proc quoted-printable {-mode what __ignored__ chunk} {
- return [mime::qp_$what $chunk]
- }
-
- if {$::major < 2} {
- # md5 v1, result is hex string ready for use.
- proc md5 {__ignored__ string} {
- return [md5::md5 $string]
- }
- } else {
- # md5 v2, need option to get hex string
- proc md5 {__ignored__ string} {
- return [md5::md5 -hex $string]
- }
- }
- }
-
- unset ::major
-}
-
-#
-# state variables:
-#
-# canonicalP: input is in its canonical form
-# content: type/subtype
-# params: dictionary (keys are lower-case)
-# encoding: transfer encoding
-# version: MIME-version
-# header: dictionary (keys are lower-case)
-# lowerL: list of header keys, lower-case
-# mixedL: list of header keys, mixed-case
-# value: either "file", "parts", or "string"
-#
-# file: input file
-# fd: cached file-descriptor, typically for root
-# root: token for top-level part, for (distant) subordinates
-# offset: number of octets from beginning of file/string
-# count: length in octets of (encoded) content
-#
-# parts: list of bodies (tokens)
-#
-# string: input string
-#
-# cid: last child-id assigned
-#
-
-
-namespace eval ::mime {
- variable mime
- array set mime {uid 0 cid 0}
-
- # RFC 822 lexemes
- variable addrtokenL
- lappend addrtokenL \; , < > : . ( ) @ \" \[ ] \\
- variable addrlexemeL {
- LX_SEMICOLON LX_COMMA
- LX_LBRACKET LX_RBRACKET
- LX_COLON LX_DOT
- LX_LPAREN LX_RPAREN
- LX_ATSIGN LX_QUOTE
- LX_LSQUARE LX_RSQUARE
- LX_QUOTE
- }
-
- # RFC 2045 lexemes
- variable typetokenL
- lappend typetokenL \; , < > : ? ( ) @ \" \[ \] = / \\
- variable typelexemeL {
- LX_SEMICOLON LX_COMMA
- LX_LBRACKET LX_RBRACKET
- LX_COLON LX_QUESTION
- LX_LPAREN LX_RPAREN
- LX_ATSIGN LX_QUOTE
- LX_LSQUARE LX_RSQUARE
- LX_EQUALS LX_SOLIDUS
- LX_QUOTE
- }
-
- variable encList {
- ascii US-ASCII
- big5 Big5
- cp1250 Windows-1250
- cp1251 Windows-1251
- cp1252 Windows-1252
- cp1253 Windows-1253
- cp1254 Windows-1254
- cp1255 Windows-1255
- cp1256 Windows-1256
- cp1257 Windows-1257
- cp1258 Windows-1258
- cp437 IBM437
- cp737 {}
- cp775 IBM775
- cp850 IBM850
- cp852 IBM852
- cp855 IBM855
- cp857 IBM857
- cp860 IBM860
- cp861 IBM861
- cp862 IBM862
- cp863 IBM863
- cp864 IBM864
- cp865 IBM865
- cp866 IBM866
- cp869 IBM869
- cp874 {}
- cp932 {}
- cp936 GBK
- cp949 {}
- cp950 {}
- dingbats {}
- ebcdic {}
- euc-cn EUC-CN
- euc-jp EUC-JP
- euc-kr EUC-KR
- gb12345 GB12345
- gb1988 GB1988
- gb2312 GB2312
- iso2022 ISO-2022
- iso2022-jp ISO-2022-JP
- iso2022-kr ISO-2022-KR
- iso8859-1 ISO-8859-1
- iso8859-2 ISO-8859-2
- iso8859-3 ISO-8859-3
- iso8859-4 ISO-8859-4
- iso8859-5 ISO-8859-5
- iso8859-6 ISO-8859-6
- iso8859-7 ISO-8859-7
- iso8859-8 ISO-8859-8
- iso8859-9 ISO-8859-9
- iso8859-10 ISO-8859-10
- iso8859-13 ISO-8859-13
- iso8859-14 ISO-8859-14
- iso8859-15 ISO-8859-15
- iso8859-16 ISO-8859-16
- jis0201 JIS_X0201
- jis0208 JIS_C6226-1983
- jis0212 JIS_X0212-1990
- koi8-r KOI8-R
- koi8-u KOI8-U
- ksc5601 KS_C_5601-1987
- macCentEuro {}
- macCroatian {}
- macCyrillic {}
- macDingbats {}
- macGreek {}
- macIceland {}
- macJapan {}
- macRoman {}
- macRomania {}
- macThai {}
- macTurkish {}
- macUkraine {}
- shiftjis Shift_JIS
- symbol {}
- tis-620 TIS-620
- unicode {}
- utf-8 UTF-8
- }
-
- variable encodings
- array set encodings $encList
- variable reversemap
- # Initialized at the bottom of the file
-
- variable encAliasList {
- ascii ANSI_X3.4-1968
- ascii iso-ir-6
- ascii ANSI_X3.4-1986
- ascii ISO_646.irv:1991
- ascii ASCII
- ascii ISO646-US
- ascii us
- ascii IBM367
- ascii cp367
- cp437 cp437
- cp437 437
- cp775 cp775
- cp850 cp850
- cp850 850
- cp852 cp852
- cp852 852
- cp855 cp855
- cp855 855
- cp857 cp857
- cp857 857
- cp860 cp860
- cp860 860
- cp861 cp861
- cp861 861
- cp861 cp-is
- cp862 cp862
- cp862 862
- cp863 cp863
- cp863 863
- cp864 cp864
- cp865 cp865
- cp865 865
- cp866 cp866
- cp866 866
- cp869 cp869
- cp869 869
- cp869 cp-gr
- cp936 CP936
- cp936 MS936
- cp936 Windows-936
- iso8859-1 ISO_8859-1:1987
- iso8859-1 iso-ir-100
- iso8859-1 ISO_8859-1
- iso8859-1 latin1
- iso8859-1 l1
- iso8859-1 IBM819
- iso8859-1 CP819
- iso8859-2 ISO_8859-2:1987
- iso8859-2 iso-ir-101
- iso8859-2 ISO_8859-2
- iso8859-2 latin2
- iso8859-2 l2
- iso8859-3 ISO_8859-3:1988
- iso8859-3 iso-ir-109
- iso8859-3 ISO_8859-3
- iso8859-3 latin3
- iso8859-3 l3
- iso8859-4 ISO_8859-4:1988
- iso8859-4 iso-ir-110
- iso8859-4 ISO_8859-4
- iso8859-4 latin4
- iso8859-4 l4
- iso8859-5 ISO_8859-5:1988
- iso8859-5 iso-ir-144
- iso8859-5 ISO_8859-5
- iso8859-5 cyrillic
- iso8859-6 ISO_8859-6:1987
- iso8859-6 iso-ir-127
- iso8859-6 ISO_8859-6
- iso8859-6 ECMA-114
- iso8859-6 ASMO-708
- iso8859-6 arabic
- iso8859-7 ISO_8859-7:1987
- iso8859-7 iso-ir-126
- iso8859-7 ISO_8859-7
- iso8859-7 ELOT_928
- iso8859-7 ECMA-118
- iso8859-7 greek
- iso8859-7 greek8
- iso8859-8 ISO_8859-8:1988
- iso8859-8 iso-ir-138
- iso8859-8 ISO_8859-8
- iso8859-8 hebrew
- iso8859-9 ISO_8859-9:1989
- iso8859-9 iso-ir-148
- iso8859-9 ISO_8859-9
- iso8859-9 latin5
- iso8859-9 l5
- iso8859-10 iso-ir-157
- iso8859-10 l6
- iso8859-10 ISO_8859-10:1992
- iso8859-10 latin6
- iso8859-14 iso-ir-199
- iso8859-14 ISO_8859-14:1998
- iso8859-14 ISO_8859-14
- iso8859-14 latin8
- iso8859-14 iso-celtic
- iso8859-14 l8
- iso8859-15 ISO_8859-15
- iso8859-15 Latin-9
- iso8859-16 iso-ir-226
- iso8859-16 ISO_8859-16:2001
- iso8859-16 ISO_8859-16
- iso8859-16 latin10
- iso8859-16 l10
- jis0201 X0201
- jis0208 iso-ir-87
- jis0208 x0208
- jis0208 JIS_X0208-1983
- jis0212 x0212
- jis0212 iso-ir-159
- ksc5601 iso-ir-149
- ksc5601 KS_C_5601-1989
- ksc5601 KSC5601
- ksc5601 korean
- shiftjis MS_Kanji
- utf-8 UTF8
- }
-
- namespace export {*}{
- copymessage finalize getbody getheader getproperty initialize
- mapencoding parseaddress parsedatetime reversemapencoding setheader
- uniqueID
- }
-}
-
-# ::mime::initialize --
-#
-# Creates a MIME part, and returnes the MIME token for that part.
-#
-# Arguments:
-# args Args can be any one of the following:
-# ?-canonical type/subtype
-# ?-param {key value}?...
-# ?-encoding value?
-# ?-header {key value}?... ?
-# (-file name | -string value | -parts {token1 ... tokenN})
-#
-# If the -canonical option is present, then the body is in
-# canonical (raw) form and is found by consulting either the -file,
-# -string, or -parts option.
-#
-# In addition, both the -param and -header options may occur zero
-# or more times to specify "Content-Type" parameters (e.g.,
-# "charset") and header keyword/values (e.g.,
-# "Content-Disposition"), respectively.
-#
-# Also, -encoding, if present, specifies the
-# "Content-Transfer-Encoding" when copying the body.
-#
-# If the -canonical option is not present, then the MIME part
-# contained in either the -file or the -string option is parsed,
-# dynamically generating subordinates as appropriate.
-#
-# Results:
-# An initialized mime token.
-
-proc ::mime::initialize args {
- global errorCode errorInfo
-
- variable mime
-
- set token [namespace current]::[incr mime(uid)]
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- if {[catch [list mime::initializeaux $token {*}$args] result eopts]} {
- catch {mime::finalize $token -subordinates dynamic}
- return -options $eopts $result
- }
- return $token
-}
-
-# ::mime::initializeaux --
-#
-# Configures the MIME token created in mime::initialize based on
-# the arguments that mime::initialize supports.
-#
-# Arguments:
-# token The MIME token to configure.
-# args Args can be any one of the following:
-# ?-canonical type/subtype
-# ?-param {key value}?...
-# ?-encoding value?
-# ?-header {key value}?... ?
-# (-file name | -string value | -parts {token1 ... tokenN})
-#
-# Results:
-# Either configures the mime token, or throws an error.
-
-proc ::mime::initializeaux {token args} {
- global errorCode errorInfo
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- array set params [set state(params) {}]
- set state(encoding) {}
- set state(version) 1.0
-
- set state(header) {}
- set state(lowerL) {}
- set state(mixedL) {}
-
- set state(cid) 0
-
- set userheader 0
-
- set argc [llength $args]
- for {set argx 0} {$argx < $argc} {incr argx} {
- set option [lindex $args $argx]
- if {[incr argx] >= $argc} {
- error "missing argument to $option"
- }
- set value [lindex $args $argx]
-
- switch -- $option {
- -canonical {
- set state(content) [string tolower $value]
- }
-
- -param {
- if {[llength $value] != 2} {
- error "-param expects a key and a value, not $value"
- }
- set lower [string tolower [set mixed [lindex $value 0]]]
- if {[info exists params($lower)]} {
- error "the $mixed parameter may be specified at most once"
- }
-
- set params($lower) [lindex $value 1]
- set state(params) [array get params]
- }
-
- -encoding {
- set value [string tolower $value[set value {}]]
-
- switch -- $value {
- 7bit - 8bit - binary - quoted-printable - base64 {
- }
-
- default {
- error "unknown value for -encoding $state(encoding)"
- }
- }
- set state(encoding) [string tolower $value]
- }
-
- -header {
- if {[llength $value] != 2} {
- error "-header expects a key and a value, not $value"
- }
- set lower [string tolower [set mixed [lindex $value 0]]]
- if {$lower eq {content-type}} {
- error "use -canonical instead of -header $value"
- }
- if {$lower eq {content-transfer-encoding}} {
- error "use -encoding instead of -header $value"
- }
- if {$lower in {content-md5 mime-version}} {
- error {don't go there...}
- }
- if {$lower ni $state(lowerL)} {
- lappend state(lowerL) $lower
- lappend state(mixedL) $mixed
- }
-
- set userheader 1
-
- array set header $state(header)
- lappend header($lower) [lindex $value 1]
- set state(header) [array get header]
- }
-
- -file {
- set state(file) $value
- }
-
- -parts {
- set state(parts) $value
- }
-
- -string {
- set state(string) $value
-
- set state(lines) [split $value \n]
- set state(lines.count) [llength $state(lines)]
- set state(lines.current) 0
- }
-
- -root {
- # the following are internal options
-
- set state(root) $value
- }
-
- -offset {
- set state(offset) $value
- }
-
- -count {
- set state(count) $value
- }
-
- -lineslist {
- set state(lines) $value
- set state(lines.count) [llength $state(lines)]
- set state(lines.current) 0
- #state(string) is needed, but will be built when required
- set state(string) {}
- }
-
- default {
- error "unknown option $option"
- }
- }
- }
-
- #We only want one of -file, -parts or -string:
- set valueN 0
- foreach value {file parts string} {
- if {[info exists state($value)]} {
- set state(value) $value
- incr valueN
- }
- }
- if {$valueN != 1 && ![info exists state(lines)]} {
- error {specify exactly one of -file, -parts, or -string}
- }
-
- if {[set state(canonicalP) [info exists state(content)]]} {
- switch -- $state(value) {
- file {
- set state(offset) 0
- }
-
- parts {
- switch -glob -- $state(content) {
- text/*
- -
- image/*
- -
- audio/*
- -
- video/* {
- error "-canonical $state(content) and -parts do not mix"
- }
-
- default {
- if {$state(encoding) ne {}} {
- error {-encoding and -parts do not mix}
- }
- }
- }
- }
- default {# Go ahead}
- }
-
- if {[lsearch -exact $state(lowerL) content-id] < 0} {
- lappend state(lowerL) content-id
- lappend state(mixedL) Content-ID
-
- array set header $state(header)
- lappend header(content-id) [uniqueID]
- set state(header) [array get header]
- }
-
- set state(version) 1.0
- return
- }
-
- if {$state(params) ne {}} {
- error {-param requires -canonical}
- }
- if {$state(encoding) ne {}} {
- error {-encoding requires -canonical}
- }
- if {$userheader} {
- error {-header requires -canonical}
- }
- if {[info exists state(parts)]} {
- error {-parts requires -canonical}
- }
-
- if {[set fileP [info exists state(file)]]} {
- if {[set openP [info exists state(root)]]} {
- # FRINK: nocheck
- variable $state(root)
- upvar 0 $state(root) root
-
- set state(fd) $root(fd)
- } else {
- set state(root) $token
- set state(fd) [open $state(file) RDONLY]
- set state(offset) 0
- seek $state(fd) 0 end
- set state(count) [tell $state(fd)]
-
- fconfigure $state(fd) -translation binary
- }
- }
-
- set code [catch {mime::parsepart $token} result]
- set ecode $errorCode
- set einfo $errorInfo
-
- if {$fileP} {
- if {!$openP} {
- unset state(root)
- catch {close $state(fd)}
- }
- unset state(fd)
- }
-
- return -code $code -errorinfo $einfo -errorcode $ecode $result
-}
-
-# ::mime::parsepart --
-#
-# Parses the MIME headers and attempts to break up the message
-# into its various parts, creating a MIME token for each part.
-#
-# Arguments:
-# token The MIME token to parse.
-#
-# Results:
-# Throws an error if it has problems parsing the MIME token,
-# otherwise it just sets up the appropriate variables.
-
-proc ::mime::parsepart {token} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- if {[set fileP [info exists state(file)]]} {
- seek $state(fd) [set pos $state(offset)] start
- set last [expr {$state(offset) + $state(count) - 1}]
- } else {
- set string $state(string)
- }
-
- set vline {}
- while 1 {
- set blankP 0
- if {$fileP} {
- if {($pos > $last) || ([set x [gets $state(fd) line]] <= 0)} {
- set blankP 1
- } else {
- incr pos [expr {$x + 1}]
- }
- } else {
- if {$state(lines.current) >= $state(lines.count)} {
- set blankP 1
- set line {}
- } else {
- set line [lindex $state(lines) $state(lines.current)]
- incr state(lines.current)
- set x [string length $line]
- if {$x == 0} {set blankP 1}
- }
- }
-
- if {!$blankP && [string match *\r $line]} {
- set line [string range $line 0 $x-2]
- if {$x == 1} {
- set blankP 1
- }
- }
-
- if {!$blankP && (
- [string first { } $line] == 0
- ||
- [string first \t $line] == 0
- )} {
- append vline \n $line
- continue
- }
-
- if {$vline eq {}} {
- if {$blankP} {
- break
- }
-
- set vline $line
- continue
- }
-
- if {
- [set x [string first : $vline]] <= 0
- ||
- [set mixed [string trimright [
- string range $vline 0 [expr {$x - 1}]]]] eq {}
- } {
- error "improper line in header: $vline"
- }
- set value [string trim [string range $vline [expr {$x + 1}] end]]
- switch -- [set lower [string tolower $mixed]] {
- content-type {
- if {[info exists state(content)]} {
- error "multiple Content-Type fields starting with $vline"
- }
-
- if {![catch {set x [parsetype $token $value]}]} {
- set state(content) [lindex $x 0]
- set state(params) [lindex $x 1]
- }
- }
-
- content-md5 {
- }
-
- content-transfer-encoding {
- if {
- $state(encoding) ne {}
- &&
- $state(encoding) ne [string tolower $value]
- } {
- error "multiple Content-Transfer-Encoding fields starting with $vline"
- }
-
- set state(encoding) [string tolower $value]
- }
-
- mime-version {
- set state(version) $value
- }
-
- default {
- if {[lsearch -exact $state(lowerL) $lower] < 0} {
- lappend state(lowerL) $lower
- lappend state(mixedL) $mixed
- }
-
- array set header $state(header)
- lappend header($lower) $value
- set state(header) [array get header]
- }
- }
-
- if {$blankP} {
- break
- }
- set vline $line
- }
-
- if {![info exists state(content)]} {
- set state(content) text/plain
- set state(params) [list charset us-ascii]
- }
-
- if {![string match multipart/* $state(content)]} {
- if {$fileP} {
- set x [tell $state(fd)]
- incr state(count) [expr {$state(offset) - $x}]
- set state(offset) $x
- } else {
- # rebuild string, this is cheap and needed by other functions
- set state(string) [join [
- lrange $state(lines) $state(lines.current) end] \n]
- }
-
- if {[string match message/* $state(content)]} {
- # FRINK: nocheck
- variable [set child $token-[incr state(cid)]]
-
- set state(value) parts
- set state(parts) $child
- if {$fileP} {
- mime::initializeaux $child \
- -file $state(file) -root $state(root) \
- -offset $state(offset) -count $state(count)
- } else {
- if {[info exists state(encoding)]} {
- set strng [join [
- lrange $state(lines) $state(lines.current) end] \n]
- switch -- $state(encoding) {
- base64 -
- quoted-printable {
- set strng [$state(encoding) -mode decode -- $strng]
- }
- default {}
- }
- mime::initializeaux $child -string $strng
- } else {
- mime::initializeaux $child -lineslist [
- lrange $state(lines) $state(lines.current) end]
- }
- }
- }
-
- return
- }
-
- set state(value) parts
-
- set boundary {}
- foreach {k v} $state(params) {
- if {$k eq {boundary}} {
- set boundary $v
- break
- }
- }
- if {$boundary eq {}} {
- error "boundary parameter is missing in $state(content)"
- }
- if {[string trim $boundary] eq {}} {
- error "boundary parameter is empty in $state(content)"
- }
-
- if {$fileP} {
- set pos [tell $state(fd)]
- # This variable is like 'start', for the reasons laid out
- # below, in the other branch of this conditional.
- set initialpos $pos
- } else {
- # This variable is like 'start', a list of lines in the
- # part. This record is made even before we find a starting
- # boundary and used if we run into the terminating boundary
- # before a starting boundary was found. In that case the lines
- # before the terminator as recorded by tracelines are seen as
- # the part, or at least we attempt to parse them as a
- # part. See the forceoctet and nochild flags later. We cannot
- # use 'start' as that records lines only after the starting
- # boundary was found.
- set tracelines [list]
- }
-
- set inP 0
- set moreP 1
- set forceoctet 0
- while {$moreP} {
- if {$fileP} {
- if {$pos > $last} {
- # We have run over the end of the part per the outer
- # information without finding a terminating boundary.
- # We now fake the boundary and force the parser to
- # give any new part coming of this a mime-type of
- # application/octet-stream regardless of header
- # information.
- set line "--$boundary--"
- set x [string length $line]
- set forceoctet 1
- } else {
- if {[set x [gets $state(fd) line]] < 0} {
- error "end-of-file encountered while parsing $state(content)"
- }
- }
- incr pos [expr {$x + 1}]
- } else {
- if {$state(lines.current) >= $state(lines.count)} {
- error "end-of-string encountered while parsing $state(content)"
- } else {
- set line [lindex $state(lines) $state(lines.current)]
- incr state(lines.current)
- set x [string length $line]
- }
- set x [string length $line]
- }
- if {[string last \r $line] == $x - 1} {
- set line [string range $line 0 [expr {$x - 2}]]
- set crlf 2
- } else {
- set crlf 1
- }
-
- if {[string first --$boundary $line] != 0} {
- if {$inP && !$fileP} {
- lappend start $line
- }
- continue
- } else {
- lappend tracelines $line
- }
-
- if {!$inP} {
- # Haven't seen the starting boundary yet. Check if the
- # current line contains this starting boundary.
-
- if {$line eq "--$boundary"} {
- # Yes. Switch parser state to now search for the
- # terminating boundary of the part and record where
- # the part begins (or initialize the recorder for the
- # lines in the part).
- set inP 1
- if {$fileP} {
- set start $pos
- } else {
- set start [list]
- }
- continue
- } elseif {$line eq "--$boundary--"} {
- # We just saw a terminating boundary before we ever
- # saw the starting boundary of a part. This forces us
- # to stop parsing, we do this by forcing the parser
- # into an accepting state. We will try to create a
- # child part based on faked start position or recorded
- # lines, or, if that fails, let the current part have
- # no children.
-
- # As an example note the test case mime-3.7 and the
- # referenced file "badmail1.txt".
-
- set inP 1
- if {$fileP} {
- set start $initialpos
- } else {
- set start $tracelines
- }
- set forceoctet 1
- # Fall through. This brings to the creation of the new
- # part instead of searching further and possible
- # running over the end.
- } else {
- continue
- }
- }
-
- # Looking for the end of the current part. We accept both a
- # terminating boundary and the starting boundary of the next
- # part as the end of the current part.
-
- if {[set moreP [string compare $line --$boundary--]]
- && $line ne "--$boundary"} {
-
- # The current part has not ended, so we record the line
- # if we are inside a part and doing string parsing.
- if {$inP && !$fileP} {
- lappend start $line
- }
- continue
- }
-
- # The current part has ended. We now determine the exact
- # boundaries, create a mime part object for it and recursively
- # parse it deeper as part of that action.
-
- # FRINK: nocheck
- variable [set child $token-[incr state(cid)]]
-
- lappend state(parts) $child
-
- set nochild 0
- if {$fileP} {
- if {[set count [expr {$pos - ($start + $x + $crlf + 1)}]] < 0} {
- set count 0
- }
- if {$forceoctet} {
- set ::errorInfo {}
- if {[catch {
- mime::initializeaux $child \
- -file $state(file) -root $state(root) \
- -offset $start -count $count
- }]} {
- set nochild 1
- set state(parts) [lrange $state(parts) 0 end-1]
- } } else {
- mime::initializeaux $child \
- -file $state(file) -root $state(root) \
- -offset $start -count $count
- }
- seek $state(fd) [set start $pos] start
- } else {
- if {$forceoctet} {
- if {[catch {
- mime::initializeaux $child -lineslist $start
- }]} {
- set nochild 1
- set state(parts) [lrange $state(parts) 0 end-1]
- }
- } else {
- mime::initializeaux $child -lineslist $start
- }
- set start {}
- }
- if {$forceoctet && !$nochild} {
- variable $child
- upvar 0 $child childstate
- set childstate(content) application/octet-stream
- }
- set forceoctet 0
- }
-}
-
-# ::mime::parsetype --
-#
-# Parses the string passed in and identifies the content-type and
-# params strings.
-#
-# Arguments:
-# token The MIME token to parse.
-# string The content-type string that should be parsed.
-#
-# Results:
-# Returns the content and params for the string as a two element
-# tcl list.
-
-proc ::mime::parsetype {token string} {
- global errorCode errorInfo
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- variable typetokenL
- variable typelexemeL
-
- set state(input) $string
- set state(buffer) {}
- set state(lastC) LX_END
- set state(comment) {}
- set state(tokenL) $typetokenL
- set state(lexemeL) $typelexemeL
-
- set code [catch {mime::parsetypeaux $token $string} result]
- set ecode $errorCode
- set einfo $errorInfo
-
- unset {*}{
- state(input)
- state(buffer)
- state(lastC)
- state(comment)
- state(tokenL)
- state(lexemeL)
- }
-
- return -code $code -errorinfo $einfo -errorcode $ecode $result
-}
-
-# ::mime::parsetypeaux --
-#
-# A helper function for mime::parsetype. Parses the specified
-# string looking for the content type and params.
-#
-# Arguments:
-# token The MIME token to parse.
-# string The content-type string that should be parsed.
-#
-# Results:
-# Returns the content and params for the string as a two element
-# tcl list.
-
-proc ::mime::parsetypeaux {token string} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- if {[parselexeme $token] ne {LX_ATOM}} {
- error [format {expecting type (found %s)} $state(buffer)]
- }
- set type [string tolower $state(buffer)]
-
- switch -- [parselexeme $token] {
- LX_SOLIDUS {
- }
-
- LX_END {
- if {$type ne {message}} {
- error "expecting type/subtype (found $type)"
- }
-
- return [list message/rfc822 {}]
- }
-
- default {
- error [format "expecting \"/\" (found %s)" $state(buffer)]
- }
- }
-
- if {[parselexeme $token] ne {LX_ATOM}} {
- error [format "expecting subtype (found %s)" $state(buffer)]
- }
- append type [string tolower /$state(buffer)]
-
- array set params {}
- while 1 {
- switch -- [parselexeme $token] {
- LX_END {
- return [list $type [array get params]]
- }
-
- LX_SEMICOLON {
- }
-
- default {
- error [format "expecting \";\" (found %s)" $state(buffer)]
- }
- }
-
- switch -- [parselexeme $token] {
- LX_END {
- return [list $type [array get params]]
- }
-
- LX_ATOM {
- }
-
- default {
- error [format "expecting attribute (found %s)" $state(buffer)]
- }
- }
-
- set attribute [string tolower $state(buffer)]
-
- if {[parselexeme $token] ne {LX_EQUALS}} {
- error [format {expecting "=" (found %s)} $state(buffer)]
- }
-
- switch -- [parselexeme $token] {
- LX_ATOM {
- }
-
- LX_QSTRING {
- set state(buffer) [
- string range $state(buffer) 1 [
- expr {[string length $state(buffer)] - 2}]]
- }
-
- default {
- error [format {expecting value (found %s)} $state(buffer)]
- }
- }
- set params($attribute) $state(buffer)
- }
-}
-
-# ::mime::finalize --
-#
-# mime::finalize destroys a MIME part.
-#
-# If the -subordinates option is present, it specifies which
-# subordinates should also be destroyed. The default value is
-# "dynamic".
-#
-# Arguments:
-# token The MIME token to parse.
-# args Args can be optionally be of the following form:
-# ?-subordinates "all" | "dynamic" | "none"?
-#
-# Results:
-# Returns an empty string.
-
-proc ::mime::finalize {token args} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- array set options [list -subordinates dynamic]
- array set options $args
-
- switch -- $options(-subordinates) {
- all {
- #TODO: this code path is untested
- if {$state(value) eq {parts}} {
- foreach part $state(parts) {
- eval [linsert $args 0 mime::finalize $part]
- }
- }
- }
-
- dynamic {
- for {set cid $state(cid)} {$cid > 0} {incr cid -1} {
- eval [linsert $args 0 mime::finalize $token-$cid]
- }
- }
-
- none {
- }
-
- default {
- error "unknown value for -subordinates $options(-subordinates)"
- }
- }
-
- foreach name [array names state] {
- unset state($name)
- }
- # FRINK: nocheck
- unset $token
-}
-
-# ::mime::getproperty --
-#
-# mime::getproperty returns the properties of a MIME part.
-#
-# The properties are:
-#
-# property value
-# ======== =====
-# content the type/subtype describing the content
-# encoding the "Content-Transfer-Encoding"
-# params a list of "Content-Type" parameters
-# parts a list of tokens for the part's subordinates
-# size the approximate size of the content (unencoded)
-#
-# The "parts" property is present only if the MIME part has
-# subordinates.
-#
-# If mime::getproperty is invoked with the name of a specific
-# property, then the corresponding value is returned; instead, if
-# -names is specified, a list of all properties is returned;
-# otherwise, a dictionary of properties is returned.
-#
-# Arguments:
-# token The MIME token to parse.
-# property One of 'content', 'encoding', 'params', 'parts', and
-# 'size'. Defaults to returning a dictionary of
-# properties.
-#
-# Results:
-# Returns the properties of a MIME part
-
-proc ::mime::getproperty {token {property {}}} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- switch -- $property {
- {} {
- array set properties [list content $state(content) \
- encoding $state(encoding) \
- params $state(params) \
- size [getsize $token]]
- if {[info exists state(parts)]} {
- set properties(parts) $state(parts)
- }
-
- return [array get properties]
- }
-
- -names {
- set names [list content encoding params]
- if {[info exists state(parts)]} {
- lappend names parts
- }
-
- return $names
- }
-
- content
- -
- encoding
- -
- params {
- return $state($property)
- }
-
- parts {
- if {![info exists state(parts)]} {
- error {MIME part is a leaf}
- }
-
- return $state(parts)
- }
-
- size {
- return [getsize $token]
- }
-
- default {
- error "unknown property $property"
- }
- }
-}
-
-# ::mime::getsize --
-#
-# Determine the size (in bytes) of a MIME part/token
-#
-# Arguments:
-# token The MIME token to parse.
-#
-# Results:
-# Returns the size in bytes of the MIME token.
-
-proc ::mime::getsize {token} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- switch -- $state(value)/$state(canonicalP) {
- file/0 {
- set size $state(count)
- }
-
- file/1 {
- return [file size $state(file)]
- }
-
- parts/0
- -
- parts/1 {
- set size 0
- foreach part $state(parts) {
- incr size [getsize $part]
- }
-
- return $size
- }
-
- string/0 {
- set size [string length $state(string)]
- }
-
- string/1 {
- return [string length $state(string)]
- }
- default {
- error "Unknown combination \"$state(value)/$state(canonicalP)\""
- }
- }
-
- if {$state(encoding) eq {base64}} {
- set size [expr {($size * 3 + 2) / 4}]
- }
-
- return $size
-}
-
-
-proc ::mime::getContentType token {
- variable $token
- upvar 0 $token state
- set res $state(content)
-
- set boundary {}
- foreach {k v} $state(params) {
- if {$k eq {boundary}} {
- set boundary $v
- }
- append res ";\n $k=\"$v\""
- }
-
- # Save boundary separate from the params
- set state(boundary) $boundary
-
- if {([string match multipart/* $state(content)]) \
- && ($boundary eq {})} {
- # we're doing everything in one pass...
- set key [clock seconds]$token[info hostname][array get state]
- set seqno 8
- while {[incr seqno -1] >= 0} {
- set key [md5 -- $key]
- }
- set boundary "----- =_[string trim [base64 -mode encode -- $key]]"
-
- set state(boundary) $boundary
-
- append res ";\n boundary=\"$boundary\""
- }
- return $res
-}
-
-# ::mime::getheader --
-#
-# mime::getheader returns the header of a MIME part.
-#
-# A header consists of zero or more key/value pairs. Each value is a
-# list containing one or more strings.
-#
-# If mime::getheader is invoked with the name of a specific key, then
-# a list containing the corresponding value(s) is returned; instead,
-# if -names is specified, a list of all keys is returned; otherwise, a
-# dictionary is returned. Note that when a
-# key is specified (e.g., "Subject"), the list returned usually
-# contains exactly one string; however, some keys (e.g., "Received")
-# often occur more than once in the header, accordingly the list
-# returned usually contains more than one string.
-#
-# Arguments:
-# token The MIME token to parse.
-# key Either a key or '-names'. If it is '-names' a list
-# of all keys is returned.
-#
-# Results:
-# Returns the header of a MIME part.
-
-proc ::mime::getheader {token {key {}}} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- array set header $state(header)
- switch -- $key {
- {} {
- set result {}
- lappend result MIME-Version $state(version)
- foreach lower $state(lowerL) mixed $state(mixedL) {
- foreach value $header($lower) {
- lappend result $mixed $value
- }
- }
- set tencoding [getTransferEncoding $token]
- if {$tencoding ne {}} {
- lappend result Content-Transfer-Encoding $tencoding
- }
- lappend result Content-Type [getContentType $token]
- return $result
- }
-
- -names {
- return $state(mixedL)
- }
-
- default {
- set lower [string tolower $key]
-
- switch $lower {
- content-transfer-encoding {
- return [getTransferEncoding $token]
- }
- content-type {
- return [list [getContentType $token]]
- }
- mime-version {
- return [list $state(version)]
- }
- default {
- if {![info exists header($lower)]} {
- error "key $key not in header"
- }
- return $header($lower)
- }
- }
- }
- }
-}
-
-
-proc ::mime::getTransferEncoding token {
- variable $token
- upvar 0 $token state
- set res {}
- if {[set encoding $state(encoding)] eq {}} {
- set encoding [encoding $token]
- }
- if {$encoding ne {}} {
- set res $encoding
- }
- switch -- $encoding {
- base64
- -
- quoted-printable {
- set converter $encoding
- }
- 7bit - 8bit - binary - {} {
- # Bugfix for [#477088], also [#539952]
- # Go ahead
- }
- default {
- error "Can't handle content encoding \"$encoding\""
- }
- }
- return $res
-}
-
-# ::mime::setheader --
-#
-# mime::setheader writes, appends to, or deletes the value associated
-# with a key in the header.
-#
-# The value for -mode is one of:
-#
-# write: the key/value is either created or overwritten (the
-# default);
-#
-# append: a new value is appended for the key (creating it as
-# necessary); or,
-#
-# delete: all values associated with the key are removed (the
-# "value" parameter is ignored).
-#
-# Regardless, mime::setheader returns the previous value associated
-# with the key.
-#
-# Arguments:
-# token The MIME token to parse.
-# key The name of the key whose value should be set.
-# value The value for the header key to be set to.
-# args An optional argument of the form:
-# ?-mode "write" | "append" | "delete"?
-#
-# Results:
-# Returns previous value associated with the specified key.
-
-proc ::mime::setheader {token key value args} {
- # FRINK: nocheck
- variable internal
- variable $token
- upvar 0 $token state
-
- array set options [list -mode write]
- array set options $args
-
- set lower [string tolower $key]
- array set header $state(header)
- if {[set x [lsearch -exact $state(lowerL) $lower]] < 0} {
- #TODO: this code path is not tested
- if {$options(-mode) eq {delete}} {
- error "key $key not in header"
- }
-
- lappend state(lowerL) $lower
- lappend state(mixedL) $key
-
- set result {}
- } else {
- set result $header($lower)
- }
- switch -- $options(-mode) {
- append - write {
- if {!$internal} {
- switch -- $lower {
- content-md5
- -
- content-type
- -
- content-transfer-encoding
- -
- mime-version {
- set values [getheader $token $lower]
- if {$value ni $values} {
- error "key $key may not be set"
- }
- }
- default {# Skip key}
- }
- }
- switch -- $options(-mode) {
- append {
- lappend header($lower) $value
- }
- write {
- set header($lower) [list $value]
- }
- }
- }
- delete {
- unset header($lower)
- set state(lowerL) [lreplace $state(lowerL) $x $x]
- set state(mixedL) [lreplace $state(mixedL) $x $x]
- }
-
- default {
- error "unknown value for -mode $options(-mode)"
- }
- }
-
- set state(header) [array get header]
- return $result
-}
-
-# ::mime::getbody --
-#
-# mime::getbody returns the body of a leaf MIME part in canonical form.
-#
-# If the -command option is present, then it is repeatedly invoked
-# with a fragment of the body as this:
-#
-# uplevel #0 $callback [list "data" $fragment]
-#
-# (The -blocksize option, if present, specifies the maximum size of
-# each fragment passed to the callback.)
-# When the end of the body is reached, the callback is invoked as:
-#
-# uplevel #0 $callback "end"
-#
-# Alternatively, if an error occurs, the callback is invoked as:
-#
-# uplevel #0 $callback [list "error" reason]
-#
-# Regardless, the return value of the final invocation of the callback
-# is propagated upwards by mime::getbody.
-#
-# If the -command option is absent, then the return value of
-# mime::getbody is a string containing the MIME part's entire body.
-#
-# Arguments:
-# token The MIME token to parse.
-# args Optional arguments of the form:
-# ?-decode? ?-command callback ?-blocksize octets? ?
-#
-# Results:
-# Returns a string containing the MIME part's entire body, or
-# if '-command' is specified, the return value of the command
-# is returned.
-
-proc ::mime::getbody {token args} {
- global errorCode errorInfo
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- set decode 0
- if {[set pos [lsearch -exact $args -decode]] >= 0} {
- set decode 1
- set args [lreplace $args $pos $pos]
- }
-
- array set options [list -command [
- list mime::getbodyaux $token] -blocksize 4096]
- array set options $args
- if {$options(-blocksize) < 1} {
- error "-blocksize expects a positive integer, not $options(-blocksize)"
- }
-
- set code 0
- set ecode {}
- set einfo {}
-
- switch -- $state(value)/$state(canonicalP) {
- file/0 {
- set fd [open $state(file) RDONLY]
-
- set code [catch {
- fconfigure $fd -translation binary
- seek $fd [set pos $state(offset)] start
- set last [expr {$state(offset) + $state(count) - 1}]
-
- set fragment {}
- while {$pos <= $last} {
- if {[set cc [
- expr {($last - $pos) + 1}]] > $options(-blocksize)} {
- set cc $options(-blocksize)
- }
- incr pos [set len [
- string length [set chunk [read $fd $cc]]]]
- switch -exact -- $state(encoding) {
- base64
- -
- quoted-printable {
- if {([set x [string last \n $chunk]] > 0) \
- && ($x + 1 != $len)} {
- set chunk [string range $chunk 0 $x]
- seek $fd [incr pos [expr {($x + 1) - $len}]] start
- }
- set chunk [
- $state(encoding) -mode decode -- $chunk]
- }
- 7bit - 8bit - binary - {} {
- # Bugfix for [#477088]
- # Go ahead, leave chunk alone
- }
- default {
- error "Can't handle content encoding \"$state(encoding)\""
- }
- }
- append fragment $chunk
-
- set cc [expr {$options(-blocksize) - 1}]
- while {[string length $fragment] > $options(-blocksize)} {
- uplevel #0 $options(-command) [
- list data [string range $fragment 0 $cc]]
-
- set fragment [
- string range $fragment $options(-blocksize) end]
- }
- }
- if {[string length $fragment] > 0} {
- uplevel #0 $options(-command) [list data $fragment]
- }
- } result]
- set ecode $errorCode
- set einfo $errorInfo
-
- catch {close $fd}
- }
-
- file/1 {
- set fd [open $state(file) RDONLY]
-
- set code [catch {
- fconfigure $fd -translation binary
-
- while {[string length [
- set fragment [read $fd $options(-blocksize)]]] > 0} {
- uplevel #0 $options(-command) [list data $fragment]
- }
- } result]
- set ecode $errorCode
- set einfo $errorInfo
-
- catch {close $fd}
- }
-
- parts/0
- -
- parts/1 {
- error {MIME part isn't a leaf}
- }
-
- string/0
- -
- string/1 {
- switch -- $state(encoding)/$state(canonicalP) {
- base64/0
- -
- quoted-printable/0 {
- set fragment [
- $state(encoding) -mode decode -- $state(string)]
- }
-
- default {
- # Not a bugfix for [#477088], but clarification
- # This handles no-encoding, 7bit, 8bit, and binary.
- set fragment $state(string)
- }
- }
-
- set code [catch {
- set cc [expr {$options(-blocksize) -1}]
- while {[string length $fragment] > $options(-blocksize)} {
- uplevel #0 $options(-command) [
- list data [string range $fragment 0 $cc]]
-
- set fragment [
- string range $fragment $options(-blocksize) end]
- }
- if {[string length $fragment] > 0} {
- uplevel #0 $options(-command) [list data $fragment]
- }
- } result]
- set ecode $errorCode
- set einfo $errorInfo
- }
- default {
- error "Unknown combination \"$state(value)/$state(canonicalP)\""
- }
- }
-
- set code [catch {
- if {$code} {
- uplevel #0 $options(-command) [list error $result]
- } else {
- uplevel #0 $options(-command) [list end]
- }
- } result]
- set ecode $errorCode
- set einfo $errorInfo
-
- if {$code} {
- return -code $code -errorinfo $einfo -errorcode $ecode $result
- }
-
- if {$decode} {
- array set params [mime::getproperty $token params]
-
- if {[info exists params(charset)]} {
- set charset $params(charset)
- } else {
- set charset US-ASCII
- }
-
- set enc [reversemapencoding $charset]
- if {$enc ne {}} {
- set result [::encoding convertfrom $enc $result]
- } else {
- return -code error "-decode failed: can't reversemap charset $charset"
- }
- }
-
- return $result
-}
-
-# ::mime::getbodyaux --
-#
-# Builds up the body of the message, fragment by fragment. When
-# the entire message has been retrieved, it is returned.
-#
-# Arguments:
-# token The MIME token to parse.
-# reason One of 'data', 'end', or 'error'.
-# fragment The section of data data fragment to extract a
-# string from.
-#
-# Results:
-# Returns nothing, except when called with the 'end' argument
-# in which case it returns a string that contains all of the
-# data that 'getbodyaux' has been called with. Will throw an
-# error if it is called with the reason of 'error'.
-
-proc ::mime::getbodyaux {token reason {fragment {}}} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- switch $reason {
- data {
- append state(getbody) $fragment
- return {}
- }
-
- end {
- if {[info exists state(getbody)]} {
- set result $state(getbody)
- unset state(getbody)
- } else {
- set result {}
- }
-
- return $result
- }
-
- error {
- catch {unset state(getbody)}
- error $reason
- }
-
- default {
- error "Unknown reason \"$reason\""
- }
- }
-}
-
-# ::mime::copymessage --
-#
-# mime::copymessage copies the MIME part to the specified channel.
-#
-# mime::copymessage operates synchronously, and uses fileevent to
-# allow asynchronous operations to proceed independently.
-#
-# Arguments:
-# token The MIME token to parse.
-# channel The channel to copy the message to.
-#
-# Results:
-# Returns nothing unless an error is thrown while the message
-# is being written to the channel.
-
-proc ::mime::copymessage {token channel} {
- global errorCode errorInfo
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- set openP [info exists state(fd)]
-
- set code [catch {mime::copymessageaux $token $channel} result]
- set ecode $errorCode
- set einfo $errorInfo
-
- if {!$openP && [info exists state(fd)]} {
- if {![info exists state(root)]} {
- catch {close $state(fd)}
- }
- unset state(fd)
- }
-
- return -code $code -errorinfo $einfo -errorcode $ecode $result
-}
-
-# ::mime::copymessageaux --
-#
-# mime::copymessageaux copies the MIME part to the specified channel.
-#
-# Arguments:
-# token The MIME token to parse.
-# channel The channel to copy the message to.
-#
-# Results:
-# Returns nothing unless an error is thrown while the message
-# is being written to the channel.
-
-proc ::mime::copymessageaux {token channel} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- array set header $state(header)
-
- set result {}
- foreach {mixed value} [getheader $token] {
- puts $channel "$mixed: $value"
- }
-
- set boundary $state(boundary) ;# computed by `getheader`
-
- set converter {}
- set encoding {}
- if {$state(value) ne {parts}} {
- if {$state(canonicalP)} {
- if {[set encoding $state(encoding)] eq {}} {
- set encoding [encoding $token]
- }
- if {$encoding ne {}} {
- puts $channel "Content-Transfer-Encoding: $encoding"
- }
- switch -- $encoding {
- base64
- -
- quoted-printable {
- set converter $encoding
- }
- 7bit - 8bit - binary - {} {
- # Bugfix for [#477088], also [#539952]
- # Go ahead
- }
- default {
- error "Can't handle content encoding \"$encoding\""
- }
- }
- }
- }
-
- if {[info exists state(error)]} {
- unset state(error)
- }
-
- switch -- $state(value) {
- file {
- set closeP 1
- if {[info exists state(root)]} {
- # FRINK: nocheck
- variable $state(root)
- upvar 0 $state(root) root
-
- if {[info exists root(fd)]} {
- set fd $root(fd)
- set closeP 0
- } else {
- set fd [set state(fd) [open $state(file) RDONLY]]
- }
- set size $state(count)
- } else {
- set fd [set state(fd) [open $state(file) RDONLY]]
- # read until eof
- set size -1
- }
- seek $fd $state(offset) start
- if {$closeP} {
- fconfigure $fd -translation binary
- }
-
- puts $channel {}
-
- while {$size != 0 && ![eof $fd]} {
- if {$size < 0 || $size > 32766} {
- set X [read $fd 32766]
- } else {
- set X [read $fd $size]
- }
- if {$size > 0} {
- set size [expr {$size - [string length $X]}]
- }
- if {$converter eq {}} {
- puts -nonewline $channel $X
- } else {
- puts -nonewline $channel [$converter -mode encode -- $X]
- }
- }
-
- if {$closeP} {
- catch {close $state(fd)}
- unset state(fd)
- }
- }
-
- parts {
- if {
- ![info exists state(root)]
- &&
- [info exists state(file)]
- } {
- set state(fd) [open $state(file) RDONLY]
- fconfigure $state(fd) -translation binary
- }
-
- switch -glob -- $state(content) {
- message/* {
- puts $channel {}
- foreach part $state(parts) {
- mime::copymessage $part $channel
- break
- }
- }
-
- default {
- # Note RFC 2046: See buildmessageaux for details.
- #
- # The boundary delimiter MUST occur at the
- # beginning of a line, i.e., following a CRLF, and
- # the initial CRLF is considered to be attached to
- # the boundary delimiter line rather than part of
- # the preceding part.
- #
- # - The above means that the CRLF before $boundary
- # is needed per the RFC, and the parts must not
- # have a closing CRLF of their own. See Tcllib bug
- # 1213527, and patch 1254934 for the problems when
- # both file/string branches added CRLF after the
- # body parts.
-
-
- foreach part $state(parts) {
- puts $channel \n--$boundary
- mime::copymessage $part $channel
- }
- puts $channel \n--$boundary--
- }
- }
-
- if {[info exists state(fd)]} {
- catch {close $state(fd)}
- unset state(fd)
- }
- }
-
- string {
- if {[catch {fconfigure $channel -buffersize} blocksize]} {
- set blocksize 4096
- } elseif {$blocksize < 512} {
- set blocksize 512
- }
- set blocksize [expr {($blocksize / 4) * 3}]
-
- # [893516]
- fconfigure $channel -buffersize $blocksize
-
- puts $channel {}
-
- #TODO: tests don't cover these paths
- if {$converter eq {}} {
- puts -nonewline $channel $state(string)
- } else {
- puts -nonewline $channel [$converter -mode encode -- $state(string)]
- }
- }
- default {
- error "Unknown value \"$state(value)\""
- }
- }
-
- flush $channel
-
- if {[info exists state(error)]} {
- error $state(error)
- }
-}
-
-# ::mime::buildmessage --
-#
-# Like copymessage, but produces a string rather than writing the message into a channel.
-#
-# Arguments:
-# token The MIME token to parse.
-#
-# Results:
-# The message.
-
-proc ::mime::buildmessage token {
- global errorCode errorInfo
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- set openP [info exists state(fd)]
-
- set code [catch {mime::buildmessageaux $token} result]
- if {![info exists errorCode]} {
- set ecode {}
- } else {
- set ecode $errorCode
- }
- set einfo $errorInfo
-
- if {!$openP && [info exists state(fd)]} {
- if {![info exists state(root)]} {
- catch {close $state(fd)}
- }
- unset state(fd)
- }
-
- return -code $code -errorinfo $einfo -errorcode $ecode $result
-}
-
-
-proc ::mime::buildmessageaux token {
- set chan [tcl::chan::memchan]
- chan configure $chan -translation crlf
- copymessageaux $token $chan
- seek $chan 0
- chan configure $chan -translation binary
- set res [read $chan]
- close $chan
- return $res
-}
-
-# ::mime::encoding --
-#
-# Determines how a token is encoded.
-#
-# Arguments:
-# token The MIME token to parse.
-#
-# Results:
-# Returns the encoding of the message (the null string, base64,
-# or quoted-printable).
-
-proc ::mime::encoding {token} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- switch -glob -- $state(content) {
- audio/*
- -
- image/*
- -
- video/* {
- return base64
- }
-
- message/*
- -
- multipart/* {
- return {}
- }
- default {# Skip}
- }
-
- set asciiP 1
- set lineP 1
- switch -- $state(value) {
- file {
- set fd [open $state(file) RDONLY]
- fconfigure $fd -translation binary
-
- while {[gets $fd line] >= 0} {
- if {$asciiP} {
- set asciiP [encodingasciiP $line]
- }
- if {$lineP} {
- set lineP [encodinglineP $line]
- }
- if {(!$asciiP) && (!$lineP)} {
- break
- }
- }
-
- catch {close $fd}
- }
-
- parts {
- return {}
- }
-
- string {
- foreach line [split $state(string) "\n"] {
- if {$asciiP} {
- set asciiP [encodingasciiP $line]
- }
- if {$lineP} {
- set lineP [encodinglineP $line]
- }
- if {(!$asciiP) && (!$lineP)} {
- break
- }
- }
- }
- default {
- error "Unknown value \"$state(value)\""
- }
- }
-
- switch -glob -- $state(content) {
- text/* {
- if {!$asciiP} {
- #TODO: this path is not covered by tests
- foreach {k v} $state(params) {
- if {$k eq "charset"} {
- set v [string tolower $v]
- if {($v ne "us-ascii") \
- && (![string match {iso-8859-[1-8]} $v])} {
- return base64
- }
-
- break
- }
- }
- }
-
- if {!$lineP} {
- return quoted-printable
- }
- }
-
-
- default {
- if {(!$asciiP) || (!$lineP)} {
- return base64
- }
- }
- }
-
- return {}
-}
-
-# ::mime::encodingasciiP --
-#
-# Checks if a string is a pure ascii string, or if it has a non-standard
-# form.
-#
-# Arguments:
-# line The line to check.
-#
-# Results:
-# Returns 1 if \r only occurs at the end of lines, and if all
-# characters in the line are between the ASCII codes of 32 and 126.
-
-proc ::mime::encodingasciiP {line} {
- foreach c [split $line {}] {
- switch -- $c {
- { } - \t - \r - \n {
- }
-
- default {
- binary scan $c c c
- if {($c < 32) || ($c > 126)} {
- return 0
- }
- }
- }
- }
- if {
- [set r [string first \r $line]] < 0
- ||
- $r == {[string length $line] - 1}
- } {
- return 1
- }
-
- return 0
-}
-
-# ::mime::encodinglineP --
-#
-# Checks if a string is a line is valid to be processed.
-#
-# Arguments:
-# line The line to check.
-#
-# Results:
-# Returns 1 the line is less than 76 characters long, the line
-# contains more characters than just whitespace, the line does
-# not start with a '.', and the line does not start with 'From '.
-
-proc ::mime::encodinglineP {line} {
- if {([string length $line] > 76) \
- || ($line ne [string trimright $line]) \
- || ([string first . $line] == 0) \
- || ([string first {From } $line] == 0)} {
- return 0
- }
-
- return 1
-}
-
-# ::mime::fcopy --
-#
-# Appears to be unused.
-#
-# Arguments:
-#
-# Results:
-#
-
-proc ::mime::fcopy {token count {error {}}} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- if {$error ne {}} {
- set state(error) $error
- }
- set state(doneP) 1
-}
-
-# ::mime::scopy --
-#
-# Copy a portion of the contents of a mime token to a channel.
-#
-# Arguments:
-# token The token containing the data to copy.
-# channel The channel to write the data to.
-# offset The location in the string to start copying
-# from.
-# len The amount of data to write.
-# blocksize The block size for the write operation.
-#
-# Results:
-# The specified portion of the string in the mime token is
-# copied to the specified channel.
-
-proc ::mime::scopy {token channel offset len blocksize} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- if {$len <= 0} {
- set state(doneP) 1
- fileevent $channel writable {}
- return
- }
-
- if {[set cc $len] > $blocksize} {
- set cc $blocksize
- }
-
- if {[catch {
- puts -nonewline $channel [
- string range $state(string) $offset [expr {$offset + $cc - 1}]]
- fileevent $channel writable [
- list mime::scopy $token $channel [
- incr offset $cc] [incr len -$cc] $blocksize]
- } result]
- } {
- set state(error) $result
- set state(doneP) 1
- fileevent $channel writable {}
- }
- return
-}
-
-# ::mime::qp_encode --
-#
-# Tcl version of quote-printable encode
-#
-# Arguments:
-# string The string to quote.
-# encoded_word Boolean value to determine whether or not encoded words
-# (RFC 2047) should be handled or not. (optional)
-#
-# Results:
-# The properly quoted string is returned.
-
-proc ::mime::qp_encode {string {encoded_word 0} {no_softbreak 0}} {
- # 8.1+ improved string manipulation routines used.
- # Replace outlying characters, characters that would normally
- # be munged by EBCDIC gateways, and special Tcl characters "[\]{}
- # with =xx sequence
-
- if {$encoded_word} {
- # Special processing for encoded words (RFC 2047)
- set regexp {[\x00-\x08\x0B-\x1E\x21-\x24\x3D\x40\x5B-\x5E\x60\x7B-\xFF\x09\x5F\x3F]}
- lappend mapChars { } _
- } else {
- set regexp {[\x00-\x08\x0B-\x1E\x21-\x24\x3D\x40\x5B-\x5E\x60\x7B-\xFF]}
- }
- regsub -all -- $regexp $string {[format =%02X [scan "\\&" %c]]} string
-
- # Replace the format commands with their result
-
- set string [subst -novariables $string]
-
- # soft/hard newlines and other
- # Funky cases for SMTP compatibility
- lappend mapChars " \n" =20\n \t\n =09\n \n\.\n =2E\n "\nFrom " "\n=46rom "
-
- set string [string map $mapChars $string]
-
- # Break long lines - ugh
-
- # Implementation of FR #503336
- if {$no_softbreak} {
- set result $string
- } else {
- set result {}
- foreach line [split $string \n] {
- while {[string length $line] > 72} {
- set chunk [string range $line 0 72]
- if {[regexp -- (=|=.)$ $chunk dummy end]} {
-
- # Don't break in the middle of a code
-
- set len [expr {72 - [string length $end]}]
- set chunk [string range $line 0 $len]
- incr len
- set line [string range $line $len end]
- } else {
- set line [string range $line 73 end]
- }
- append result $chunk=\n
- }
- append result $line\n
- }
-
- # Trim off last \n, since the above code has the side-effect
- # of adding an extra \n to the encoded string and return the
- # result.
- set result [string range $result 0 end-1]
- }
-
- # If the string ends in space or tab, replace with =xx
-
- set lastChar [string index $result end]
- if {$lastChar eq { }} {
- set result [string replace $result end end =20]
- } elseif {$lastChar eq "\t"} {
- set result [string replace $result end end =09]
- }
-
- return $result
-}
-
-# ::mime::qp_decode --
-#
-# Tcl version of quote-printable decode
-#
-# Arguments:
-# string The quoted-printable string to decode.
-# encoded_word Boolean value to determine whether or not encoded words
-# (RFC 2047) should be handled or not. (optional)
-#
-# Results:
-# The decoded string is returned.
-
-proc ::mime::qp_decode {string {encoded_word 0}} {
- # 8.1+ improved string manipulation routines used.
- # Special processing for encoded words (RFC 2047)
-
- if {$encoded_word} {
- # _ == \x20, even if SPACE occupies a different code position
- set string [string map [list _ \u0020] $string]
- }
-
- # smash the white-space at the ends of lines since that must've been
- # generated by an MUA.
-
- regsub -all -- {[ \t]+\n} $string \n string
- set string [string trimright $string " \t"]
-
- # Protect the backslash for later subst and
- # smash soft newlines, has to occur after white-space smash
- # and any encoded word modification.
-
- #TODO: codepath not tested
- set string [string map [list \\ {\\} =\n {}] $string]
-
- # Decode specials
-
- regsub -all -nocase {=([a-f0-9][a-f0-9])} $string {\\u00\1} string
-
- # process \u unicode mapped chars
-
- return [subst -novariables -nocommands $string]
-}
-
-# ::mime::parseaddress --
-#
-# This was originally written circa 1982 in C. we're still using it
-# because it recognizes virtually every buggy address syntax ever
-# generated!
-#
-# mime::parseaddress takes a string containing one or more 822-style
-# address specifications and returns a list of dictionaries, for each
-# address specified in the argument.
-#
-# Each dictionary contains these properties:
-#
-# property value
-# ======== =====
-# address local@domain
-# comment 822-style comment
-# domain the domain part (rhs)
-# error non-empty on a parse error
-# group this address begins a group
-# friendly user-friendly rendering
-# local the local part (lhs)
-# memberP this address belongs to a group
-# phrase the phrase part
-# proper 822-style address specification
-# route 822-style route specification (obsolete)
-#
-# Note that one or more of these properties may be empty.
-#
-# Arguments:
-# string The address string to parse
-#
-# Results:
-# Returns a list of dictionaries, one element for each address
-# specified in the argument.
-
-proc ::mime::parseaddress {string} {
- global errorCode errorInfo
-
- variable mime
-
- set token [namespace current]::[incr mime(uid)]
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- set code [catch {mime::parseaddressaux $token $string} result]
- set ecode $errorCode
- set einfo $errorInfo
-
- foreach name [array names state] {
- unset state($name)
- }
- # FRINK: nocheck
- catch {unset $token}
-
- return -code $code -errorinfo $einfo -errorcode $ecode $result
-}
-
-# ::mime::parseaddressaux --
-#
-# This was originally written circa 1982 in C. we're still using it
-# because it recognizes virtually every buggy address syntax ever
-# generated!
-#
-# mime::parseaddressaux does the actually parsing for mime::parseaddress
-#
-# Each dictionary contains these properties:
-#
-# property value
-# ======== =====
-# address local@domain
-# comment 822-style comment
-# domain the domain part (rhs)
-# error non-empty on a parse error
-# group this address begins a group
-# friendly user-friendly rendering
-# local the local part (lhs)
-# memberP this address belongs to a group
-# phrase the phrase part
-# proper 822-style address specification
-# route 822-style route specification (obsolete)
-#
-# Note that one or more of these properties may be empty.
-#
-# Arguments:
-# token The MIME token to work from.
-# string The address string to parse
-#
-# Results:
-# Returns a list of dictionaries, one for each address specified in the
-# argument.
-
-proc ::mime::parseaddressaux {token string} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- variable addrtokenL
- variable addrlexemeL
-
- set state(input) $string
- set state(glevel) 0
- set state(buffer) {}
- set state(lastC) LX_END
- set state(tokenL) $addrtokenL
- set state(lexemeL) $addrlexemeL
-
- set result {}
- while {[addr_next $token]} {
- if {[set tail $state(domain)] ne {}} {
- set tail @$state(domain)
- } else {
- set tail @[info hostname]
- }
- if {[set address $state(local)] ne {}} {
- #TODO: this path is not covered by tests
- append address $tail
- }
-
- if {$state(phrase) ne {}} {
- #TODO: this path is not covered by tests
- set state(phrase) [string trim $state(phrase) \"]
- foreach t $state(tokenL) {
- if {[string first $t $state(phrase)] >= 0} {
- #TODO: is this quoting robust enough?
- set state(phrase) \"$state(phrase)\"
- break
- }
- }
-
- set proper "$state(phrase) <$address>"
- } else {
- set proper $address
- }
-
- if {[set friendly $state(phrase)] eq {}} {
- #TODO: this path is not covered by tests
- if {[set note $state(comment)] ne {}} {
- if {[string first ( $note] == 0} {
- set note [string trimleft [string range $note 1 end]]
- }
- if {
- [string last ) $note]
- == [set len [expr {[string length $note] - 1}]]
- } {
- set note [string range $note 0 [expr {$len - 1}]]
- }
- set friendly $note
- }
-
- if {
- $friendly eq {}
- &&
- [set mbox $state(local)] ne {}
- } {
- #TODO: this path is not covered by tests
- set mbox [string trim $mbox \"]
-
- if {[string first / $mbox] != 0} {
- set friendly $mbox
- } elseif {[set friendly [addr_x400 $mbox PN]] ne {}} {
- } elseif {
- [set friendly [addr_x400 $mbox S]] ne {}
- &&
- [set g [addr_x400 $mbox G]] ne {}
- } {
- set friendly "$g $friendly"
- }
-
- if {$friendly eq {}} {
- set friendly $mbox
- }
- }
- }
- set friendly [string trim $friendly \"]
-
- lappend result [list address $address \
- comment $state(comment) \
- domain $state(domain) \
- error $state(error) \
- friendly $friendly \
- group $state(group) \
- local $state(local) \
- memberP $state(memberP) \
- phrase $state(phrase) \
- proper $proper \
- route $state(route)]
-
- }
-
- unset {*}{
- state(input)
- state(glevel)
- state(buffer)
- state(lastC)
- state(tokenL)
- state(lexemeL)
- }
-
- return $result
-}
-
-# ::mime::addr_next --
-#
-# Locate the next address in a mime token.
-#
-# Arguments:
-# token The MIME token to work from.
-#
-# Results:
-# Returns 1 if there is another address, and 0 if there is not.
-
-proc ::mime::addr_next {token} {
- global errorCode errorInfo
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
- set nocomplain [package vsatisfies [package provide Tcl] 8.4]
- foreach prop {comment domain error group local memberP phrase route} {
- if {$nocomplain} {
- unset -nocomplain state($prop)
- } else {
- if {[catch {unset state($prop)}]} {set ::errorInfo {}}
- }
- }
-
- switch -- [set code [catch {mime::addr_specification $token} result]] {
- 0 {
- if {!$result} {
- return 0
- }
-
- switch -- $state(lastC) {
- LX_COMMA
- -
- LX_END {
- }
- default {
- # catch trailing comments...
- set lookahead $state(input)
- mime::parselexeme $token
- set state(input) $lookahead
- }
- }
- }
-
- 7 {
- set state(error) $result
-
- while {1} {
- switch -- $state(lastC) {
- LX_COMMA
- -
- LX_END {
- break
- }
-
- default {
- mime::parselexeme $token
- }
- }
- }
- }
-
- default {
- set ecode $errorCode
- set einfo $errorInfo
-
- return -code $code -errorinfo $einfo -errorcode $ecode $result
- }
- }
-
- foreach prop {comment domain error group local memberP phrase route} {
- if {![info exists state($prop)]} {
- set state($prop) {}
- }
- }
-
- return 1
-}
-
-# ::mime::addr_specification --
-#
-# Uses lookahead parsing to determine whether there is another
-# valid e-mail address or not. Throws errors if unrecognized
-# or invalid e-mail address syntax is used.
-#
-# Arguments:
-# token The MIME token to work from.
-#
-# Results:
-# Returns 1 if there is another address, and 0 if there is not.
-
-proc ::mime::addr_specification {token} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- set lookahead $state(input)
- switch -- [parselexeme $token] {
- LX_ATOM
- -
- LX_QSTRING {
- set state(phrase) $state(buffer)
- }
-
- LX_SEMICOLON {
- if {[incr state(glevel) -1] < 0} {
- return -code 7 "extraneous semi-colon"
- }
-
- catch {unset state(comment)}
- return [addr_specification $token]
- }
-
- LX_COMMA {
- catch {unset state(comment)}
- return [addr_specification $token]
- }
-
- LX_END {
- return 0
- }
-
- LX_LBRACKET {
- return [addr_routeaddr $token]
- }
-
- LX_ATSIGN {
- set state(input) $lookahead
- return [addr_routeaddr $token 0]
- }
-
- default {
- return -code 7 [
- format "unexpected character at beginning (found %s)" \
- $state(buffer)]
- }
- }
-
- switch -- [parselexeme $token] {
- LX_ATOM
- -
- LX_QSTRING {
- append state(phrase) " " $state(buffer)
-
- return [addr_phrase $token]
- }
-
- LX_LBRACKET {
- return [addr_routeaddr $token]
- }
-
- LX_COLON {
- return [addr_group $token]
- }
-
- LX_DOT {
- set state(local) "$state(phrase)$state(buffer)"
- unset state(phrase)
- mime::addr_routeaddr $token 0
- mime::addr_end $token
- }
-
- LX_ATSIGN {
- set state(memberP) $state(glevel)
- set state(local) $state(phrase)
- unset state(phrase)
- mime::addr_domain $token
- mime::addr_end $token
- }
-
- LX_SEMICOLON
- -
- LX_COMMA
- -
- LX_END {
- set state(memberP) $state(glevel)
- if {
- $state(lastC) eq "LX_SEMICOLON"
- &&
- ([incr state(glevel) -1] < 0)
- } {
- #TODO: this path is not covered by tests
- return -code 7 "extraneous semi-colon"
- }
-
- set state(local) $state(phrase)
- unset state(phrase)
- }
-
- default {
- return -code 7 [
- format "expecting mailbox (found %s)" $state(buffer)]
- }
- }
-
- return 1
-}
-
-# ::mime::addr_routeaddr --
-#
-# Parses the domain portion of an e-mail address. Finds the '@'
-# sign and then calls mime::addr_route to verify the domain.
-#
-# Arguments:
-# token The MIME token to work from.
-#
-# Results:
-# Returns 1 if there is another address, and 0 if there is not.
-
-proc ::mime::addr_routeaddr {token {checkP 1}} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- set lookahead $state(input)
- if {[parselexeme $token] eq "LX_ATSIGN"} {
- #TODO: this path is not covered by tests
- mime::addr_route $token
- } else {
- set state(input) $lookahead
- }
-
- mime::addr_local $token
-
- switch -- $state(lastC) {
- LX_ATSIGN {
- mime::addr_domain $token
- }
-
- LX_SEMICOLON
- -
- LX_RBRACKET
- -
- LX_COMMA
- -
- LX_END {
- }
-
- default {
- return -code 7 [
- format "expecting at-sign after local-part (found %s)" \
- $state(buffer)]
- }
- }
-
- if {($checkP) && ($state(lastC) ne "LX_RBRACKET")} {
- return -code 7 [
- format "expecting right-bracket (found %s)" $state(buffer)]
- }
-
- return 1
-}
-
-# ::mime::addr_route --
-#
-# Attempts to parse the portion of the e-mail address after the @.
-# Tries to verify that the domain definition has a valid form.
-#
-# Arguments:
-# token The MIME token to work from.
-#
-# Results:
-# Returns nothing if successful, and throws an error if invalid
-# syntax is found.
-
-proc ::mime::addr_route {token} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- set state(route) @
-
- while 1 {
- switch -- [parselexeme $token] {
- LX_ATOM
- -
- LX_DLITERAL {
- append state(route) $state(buffer)
- }
-
- default {
- return -code 7 \
- [format "expecting sub-route in route-part (found %s)" \
- $state(buffer)]
- }
- }
-
- switch -- [parselexeme $token] {
- LX_COMMA {
- append state(route) $state(buffer)
- while 1 {
- switch -- [parselexeme $token] {
- LX_COMMA {
- }
-
- LX_ATSIGN {
- append state(route) $state(buffer)
- break
- }
-
- default {
- return -code 7 \
- [format "expecting at-sign in route (found %s)" \
- $state(buffer)]
- }
- }
- }
- }
-
- LX_ATSIGN
- -
- LX_DOT {
- append state(route) $state(buffer)
- }
-
- LX_COLON {
- append state(route) $state(buffer)
- return
- }
-
- default {
- return -code 7 [
- format "expecting colon to terminate route (found %s)" \
- $state(buffer)]
- }
- }
- }
-}
-
-# ::mime::addr_domain --
-#
-# Attempts to parse the portion of the e-mail address after the @.
-# Tries to verify that the domain definition has a valid form.
-#
-# Arguments:
-# token The MIME token to work from.
-#
-# Results:
-# Returns nothing if successful, and throws an error if invalid
-# syntax is found.
-
-proc ::mime::addr_domain {token} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- while 1 {
- switch -- [parselexeme $token] {
- LX_ATOM
- -
- LX_DLITERAL {
- append state(domain) $state(buffer)
- }
-
- default {
- return -code 7 [
- format "expecting sub-domain in domain-part (found %s)" \
- $state(buffer)]
- }
- }
-
- switch -- [parselexeme $token] {
- LX_DOT {
- append state(domain) $state(buffer)
- }
-
- LX_ATSIGN {
- append state(local) % $state(domain)
- unset state(domain)
- }
-
- default {
- return
- }
- }
- }
-}
-
-# ::mime::addr_local --
-#
-#
-# Arguments:
-# token The MIME token to work from.
-#
-# Results:
-# Returns nothing if successful, and throws an error if invalid
-# syntax is found.
-
-proc ::mime::addr_local {token} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- set state(memberP) $state(glevel)
-
- while 1 {
- switch -- [parselexeme $token] {
- LX_ATOM
- -
- LX_QSTRING {
- append state(local) $state(buffer)
- }
-
- default {
- return -code 7 \
- [format "expecting mailbox in local-part (found %s)" \
- $state(buffer)]
- }
- }
-
- switch -- [parselexeme $token] {
- LX_DOT {
- append state(local) $state(buffer)
- }
-
- default {
- return
- }
- }
- }
-}
-
-# ::mime::addr_phrase --
-#
-#
-# Arguments:
-# token The MIME token to work from.
-#
-# Results:
-# Returns nothing if successful, and throws an error if invalid
-# syntax is found.
-
-
-proc ::mime::addr_phrase {token} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- while {1} {
- switch -- [parselexeme $token] {
- LX_ATOM
- -
- LX_QSTRING {
- append state(phrase) " " $state(buffer)
- }
-
- default {
- break
- }
- }
- }
-
- switch -- $state(lastC) {
- LX_LBRACKET {
- return [addr_routeaddr $token]
- }
-
- LX_COLON {
- return [addr_group $token]
- }
-
- LX_DOT {
- append state(phrase) $state(buffer)
- return [addr_phrase $token]
- }
-
- default {
- return -code 7 [
- format "found phrase instead of mailbox (%s%s)" \
- $state(phrase) $state(buffer)]
- }
- }
-}
-
-# ::mime::addr_group --
-#
-#
-# Arguments:
-# token The MIME token to work from.
-#
-# Results:
-# Returns nothing if successful, and throws an error if invalid
-# syntax is found.
-
-proc ::mime::addr_group {token} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- if {[incr state(glevel)] > 1} {
- return -code 7 [
- format "nested groups not allowed (found %s)" $state(phrase)]
- }
-
- set state(group) $state(phrase)
- unset state(phrase)
-
- set lookahead $state(input)
- while 1 {
- switch -- [parselexeme $token] {
- LX_SEMICOLON
- -
- LX_END {
- set state(glevel) 0
- return 1
- }
-
- LX_COMMA {
- }
-
- default {
- set state(input) $lookahead
- return [addr_specification $token]
- }
- }
- }
-}
-
-# ::mime::addr_end --
-#
-#
-# Arguments:
-# token The MIME token to work from.
-#
-# Results:
-# Returns nothing if successful, and throws an error if invalid
-# syntax is found.
-
-proc ::mime::addr_end {token} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- switch -- $state(lastC) {
- LX_SEMICOLON {
- if {[incr state(glevel) -1] < 0} {
- return -code 7 "extraneous semi-colon"
- }
- }
-
- LX_COMMA
- -
- LX_END {
- }
-
- default {
- return -code 7 [
- format "junk after local@domain (found %s)" $state(buffer)]
- }
- }
-}
-
-# ::mime::addr_x400 --
-#
-#
-# Arguments:
-# token The MIME token to work from.
-#
-# Results:
-# Returns nothing if successful, and throws an error if invalid
-# syntax is found.
-
-proc ::mime::addr_x400 {mbox key} {
- if {[set x [string first /$key= [string toupper $mbox]]] < 0} {
- return {}
- }
- set mbox [string range $mbox [expr {$x + [string length $key] + 2}] end]
-
- if {[set x [string first / $mbox]] > 0} {
- set mbox [string range $mbox 0 [expr {$x - 1}]]
- }
-
- return [string trim $mbox \"]
-}
-
-# ::mime::parsedatetime --
-#
-# Fortunately the clock command in the Tcl 8.x core does all the heavy
-# lifting for us (except for timezone calculations).
-#
-# mime::parsedatetime takes a string containing an 822-style date-time
-# specification and returns the specified property.
-#
-# The list of properties and their ranges are:
-#
-# property range
-# ======== =====
-# clock raw result of "clock scan"
-# hour 0 .. 23
-# lmonth January, February, ..., December
-# lweekday Sunday, Monday, ... Saturday
-# mday 1 .. 31
-# min 0 .. 59
-# mon 1 .. 12
-# month Jan, Feb, ..., Dec
-# proper 822-style date-time specification
-# rclock elapsed seconds between then and now
-# sec 0 .. 59
-# wday 0 .. 6 (Sun .. Mon)
-# weekday Sun, Mon, ..., Sat
-# yday 1 .. 366
-# year 1900 ...
-# zone -720 .. 720 (minutes east of GMT)
-#
-# Arguments:
-# value Either a 822-style date-time specification or '-now'
-# if the current date/time should be used.
-# property The property (from the list above) to return
-#
-# Results:
-# Returns the string value of the 'property' for the date/time that was
-# specified in 'value'.
-
-namespace eval ::mime {
- variable WDAYS_SHORT [list Sun Mon Tue Wed Thu Fri Sat]
- variable WDAYS_LONG [list Sunday Monday Tuesday Wednesday Thursday \
- Friday Saturday]
-
- # Counting months starts at 1, so just insert a dummy element
- # at index 0.
- variable MONTHS_SHORT [list {} \
- Jan Feb Mar Apr May Jun \
- Jul Aug Sep Oct Nov Dec]
- variable MONTHS_LONG [list {} \
- January February March April May June July \
- August Sepember October November December]
-}
-proc ::mime::parsedatetime {value property} {
- if {$value eq "-now"} {
- set clock [clock seconds]
- } elseif {[regexp {^(.*) ([+-])([0-9][0-9])([0-9][0-9])$} $value \
- -> value zone_sign zone_hour zone_min]
- } {
- set clock [clock scan $value -gmt 1]
- if {[info exists zone_min]} {
- set zone_min [scan $zone_min %d]
- set zone_hour [scan $zone_hour %d]
- set zone [expr {60 * ($zone_min + 60 * $zone_hour)}]
- if {$zone_sign eq "+"} {
- set zone -$zone
- }
- incr clock $zone
- }
- } else {
- set clock [clock scan $value]
- }
-
- switch -- $property {
- clock {
- return $clock
- }
-
- hour {
- set value [clock format $clock -format %H]
- }
-
- lmonth {
- variable MONTHS_LONG
- return [lindex $MONTHS_LONG \
- [scan [clock format $clock -format %m] %d]]
- }
-
- lweekday {
- variable WDAYS_LONG
- return [lindex $WDAYS_LONG [clock format $clock -format %w]]
- }
-
- mday {
- set value [clock format $clock -format %d]
- }
-
- min {
- set value [clock format $clock -format %M]
- }
-
- mon {
- set value [clock format $clock -format %m]
- }
-
- month {
- variable MONTHS_SHORT
- return [lindex $MONTHS_SHORT [
- scan [clock format $clock -format %m] %d]]
- }
-
- proper {
- set gmt [clock format $clock -format "%Y-%m-%d %H:%M:%S" -gmt true]
- if {[set diff [expr {($clock-[clock scan $gmt]) / 60}]] < 0} {
- set s -
- set diff [expr {-($diff)}]
- } else {
- set s +
- }
- set zone [format %s%02d%02d $s [
- expr {$diff / 60}] [expr {$diff % 60}]]
-
- variable WDAYS_SHORT
- set wday [lindex $WDAYS_SHORT [clock format $clock -format %w]]
- variable MONTHS_SHORT
- set mon [lindex $MONTHS_SHORT [
- scan [clock format $clock -format %m] %d]]
-
- return [
- clock format $clock -format "$wday, %d $mon %Y %H:%M:%S $zone"]
- }
-
- rclock {
- #TODO: these paths are not covered by tests
- if {$value eq "-now"} {
- return 0
- } else {
- return [expr {[clock seconds] - $clock}]
- }
- }
-
- sec {
- set value [clock format $clock -format %S]
- }
-
- wday {
- return [clock format $clock -format %w]
- }
-
- weekday {
- variable WDAYS_SHORT
- return [lindex $WDAYS_SHORT [clock format $clock -format %w]]
- }
-
- yday {
- set value [clock format $clock -format %j]
- }
-
- year {
- set value [clock format $clock -format %Y]
- }
-
- zone {
- set value [string trim [string map [list \t { }] $value]]
- if {[set x [string last { } $value]] < 0} {
- return 0
- }
- set value [string range $value [expr {$x + 1}] end]
- switch -- [set s [string index $value 0]] {
- + - - {
- if {$s eq "+"} {
- #TODO: This path is not covered by tests
- set s {}
- }
- set value [string trim [string range $value 1 end]]
- if {(
- [string length $value] != 4)
- ||
- [scan $value %2d%2d h m] != 2
- ||
- $h > 12
- ||
- $m > 59
- ||
- ($h == 12 && $m > 0)
- } {
- error "malformed timezone-specification: $value"
- }
- set value $s[expr {$h * 60 + $m}]
- }
-
- default {
- set value [string toupper $value]
- set z1 [list UT GMT EST EDT CST CDT MST MDT PST PDT]
- set z2 [list 0 0 -5 -4 -6 -5 -7 -6 -8 -7]
- if {[set x [lsearch -exact $z1 $value]] < 0} {
- error "unrecognized timezone-mnemonic: $value"
- }
- set value [expr {[lindex $z2 $x] * 60}]
- }
- }
- }
-
- date2gmt
- -
- date2local
- -
- dst
- -
- sday
- -
- szone
- -
- tzone
- -
- default {
- error "unknown property $property"
- }
- }
-
- if {[set value [string trimleft $value 0]] eq {}} {
- #TODO: this path is not covered by tests
- set value 0
- }
- return $value
-}
-
-# ::mime::uniqueID --
-#
-# Used to generate a 'globally unique identifier' for the content-id.
-# The id is built from the pid, the current time, the hostname, and
-# a counter that is incremented each time a message is sent.
-#
-# Arguments:
-#
-# Results:
-# Returns the a string that contains the globally unique identifier
-# that should be used for the Content-ID of an e-mail message.
-
-proc ::mime::uniqueID {} {
- variable mime
-
- return <[pid].[clock seconds].[incr mime(cid)]@[info hostname]>
-}
-
-# ::mime::parselexeme --
-#
-# Used to implement a lookahead parser.
-#
-# Arguments:
-# token The MIME token to operate on.
-#
-# Results:
-# Returns the next token found by the parser.
-
-proc ::mime::parselexeme {token} {
- # FRINK: nocheck
- variable $token
- upvar 0 $token state
-
- set state(input) [string trimleft $state(input)]
-
- set state(buffer) {}
- if {$state(input) eq {}} {
- set state(buffer) end-of-input
- return [set state(lastC) LX_END]
- }
-
- set c [string index $state(input) 0]
- set state(input) [string range $state(input) 1 end]
-
- if {$c eq "("} {
- set noteP 0
- set quoteP 0
-
- while 1 {
- append state(buffer) $c
-
- #TODO: some of these paths are not covered by tests
- switch -- $c/$quoteP {
- (/0 {
- incr noteP
- }
-
- \\/0 {
- set quoteP 1
- }
-
- )/0 {
- if {[incr noteP -1] < 1} {
- if {[info exists state(comment)]} {
- append state(comment) { }
- }
- append state(comment) $state(buffer)
-
- return [parselexeme $token]
- }
- }
-
- default {
- set quoteP 0
- }
- }
-
- if {[set c [string index $state(input) 0]] eq {}} {
- set state(buffer) "end-of-input during comment"
- return [set state(lastC) LX_ERR]
- }
- set state(input) [string range $state(input) 1 end]
- }
- }
-
- if {$c eq "\""} {
- set firstP 1
- set quoteP 0
-
- while 1 {
- append state(buffer) $c
-
- switch -- $c/$quoteP {
- "\\/0" {
- set quoteP 1
- }
-
- "\"/0" {
- if {!$firstP} {
- return [set state(lastC) LX_QSTRING]
- }
- set firstP 0
- }
-
- default {
- set quoteP 0
- }
- }
-
- if {[set c [string index $state(input) 0]] eq {}} {
- set state(buffer) "end-of-input during quoted-string"
- return [set state(lastC) LX_ERR]
- }
- set state(input) [string range $state(input) 1 end]
- }
- }
-
- if {$c eq {[}} {
- set quoteP 0
-
- while 1 {
- append state(buffer) $c
-
- switch -- $c/$quoteP {
- \\/0 {
- set quoteP 1
- }
-
- ]/0 {
- return [set state(lastC) LX_DLITERAL]
- }
-
- default {
- set quoteP 0
- }
- }
-
- if {[set c [string index $state(input) 0]] eq {}} {
- set state(buffer) "end-of-input during domain-literal"
- return [set state(lastC) LX_ERR]
- }
- set state(input) [string range $state(input) 1 end]
- }
- }
-
- if {[set x [lsearch -exact $state(tokenL) $c]] >= 0} {
- append state(buffer) $c
-
- return [set state(lastC) [lindex $state(lexemeL) $x]]
- }
-
- while 1 {
- append state(buffer) $c
-
- switch -- [set c [string index $state(input) 0]] {
- {} - " " - "\t" - "\n" {
- break
- }
-
- default {
- if {[lsearch -exact $state(tokenL) $c] >= 0} {
- break
- }
- }
- }
-
- set state(input) [string range $state(input) 1 end]
- }
-
- return [set state(lastC) LX_ATOM]
-}
-
-# ::mime::mapencoding --
-#
-# mime::mapencodings maps tcl encodings onto the proper names for their
-# MIME charset type. This is only done for encodings whose charset types
-# were known. The remaining encodings return {} for now.
-#
-# Arguments:
-# enc The tcl encoding to map.
-#
-# Results:
-# Returns the MIME charset type for the specified tcl encoding, or {}
-# if none is known.
-
-proc ::mime::mapencoding {enc} {
-
- variable encodings
-
- if {[info exists encodings($enc)]} {
- return $encodings($enc)
- }
- return {}
-}
-
-# ::mime::reversemapencoding --
-#
-# mime::reversemapencodings maps MIME charset types onto tcl encoding names.
-# Those that are unknown return {}.
-#
-# Arguments:
-# mimeType The MIME charset to convert into a tcl encoding type.
-#
-# Results:
-# Returns the tcl encoding name for the specified mime charset, or {}
-# if none is known.
-
-proc ::mime::reversemapencoding {mimeType} {
-
- variable reversemap
-
- set lmimeType [string tolower $mimeType]
- if {[info exists reversemap($lmimeType)]} {
- return $reversemap($lmimeType)
- }
- return {}
-}
-
-# ::mime::word_encode --
-#
-# Word encodes strings as per RFC 2047.
-#
-# Arguments:
-# charset The character set to encode the message to.
-# method The encoding method (base64 or quoted-printable).
-# string The string to encode.
-# ?-charset_encoded 0 or 1 Whether the data is already encoded
-# in the specified charset (default 1)
-# ?-maxlength maxlength The maximum length of each encoded
-# word to return (default 66)
-#
-# Results:
-# Returns a word encoded string.
-
-proc ::mime::word_encode {charset method string {args}} {
-
- variable encodings
-
- if {![info exists encodings($charset)]} {
- error "unknown charset '$charset'"
- }
-
- if {$encodings($charset) eq {}} {
- error "invalid charset '$charset'"
- }
-
- if {$method ne "base64" && $method ne "quoted-printable"} {
- error "unknown method '$method', must be base64 or quoted-printable"
- }
-
- # default to encoded and a length that won't make the Subject header to long
- array set options [list -charset_encoded 1 -maxlength 66]
- array set options $args
-
- if {$options(-charset_encoded)} {
- set unencoded_string [::encoding convertfrom $charset $string]
- } else {
- set unencoded_string $string
- }
-
- set string_length [string length $unencoded_string]
-
- if {!$string_length} {
- return {}
- }
-
- set string_bytelength [string bytelength $unencoded_string]
-
- # the 7 is for =?, ?Q?, ?= delimiters of the encoded word
- set maxlength [expr {$options(-maxlength) - [string length $encodings($charset)] - 7}]
- switch -exact -- $method {
- base64 {
- if {$maxlength < 4} {
- error "maxlength $options(-maxlength) too short for chosen charset and encoding"
- }
- set count 0
- set maxlength [expr {($maxlength / 4) * 3}]
- while {$count < $string_length} {
- set length 0
- set enc_string {}
- while {$length < $maxlength && $count < $string_length} {
- set char [string range $unencoded_string $count $count]
- set enc_char [::encoding convertto $charset $char]
- if {$length + [string length $enc_char] > $maxlength} {
- set length $maxlength
- } else {
- append enc_string $enc_char
- incr count
- incr length [string length $enc_char]
- }
- }
- set encoded_word [string map [
- list \n {}] [base64 -mode encode -- $enc_string]]
- append result "=?$encodings($charset)?B?$encoded_word?=\n "
- }
- # Trim off last "\n ", since the above code has the side-effect
- # of adding an extra "\n " to the encoded string.
-
- set result [string range $result 0 end-2]
- }
- quoted-printable {
- if {$maxlength < 1} {
- error "maxlength $options(-maxlength) too short for chosen charset and encoding"
- }
- set count 0
- while {$count < $string_length} {
- set length 0
- set encoded_word {}
- while {$length < $maxlength && $count < $string_length} {
- set char [string range $unencoded_string $count $count]
- set enc_char [::encoding convertto $charset $char]
- set qp_enc_char [qp_encode $enc_char 1]
- set qp_enc_char_length [string length $qp_enc_char]
- if {$qp_enc_char_length > $maxlength} {
- error "maxlength $options(-maxlength) too short for chosen charset and encoding"
- }
- if {
- $length + [string length $qp_enc_char] > $maxlength
- } {
- set length $maxlength
- } else {
- append encoded_word $qp_enc_char
- incr count
- incr length [string length $qp_enc_char]
- }
- }
- append result "=?$encodings($charset)?Q?$encoded_word?=\n "
- }
- # Trim off last "\n ", since the above code has the side-effect
- # of adding an extra "\n " to the encoded string.
-
- set result [string range $result 0 end-2]
- }
- {} {
- # Go ahead
- }
- default {
- error "Can't handle content encoding \"$method\""
- }
- }
- return $result
-}
-
-# ::mime::word_decode --
-#
-# Word decodes strings that have been word encoded as per RFC 2047.
-#
-# Arguments:
-# encoded The word encoded string to decode.
-#
-# Results:
-# Returns the string that has been decoded from the encoded message.
-
-proc ::mime::word_decode {encoded} {
-
- variable reversemap
-
- if {[regexp -- {=\?([^?]+)\?(.)\?([^?]*)\?=} $encoded \
- - charset method string] != 1
- } {
- error "malformed word-encoded expression '$encoded'"
- }
-
- set enc [reversemapencoding $charset]
- if {$enc eq {}} {
- error "unknown charset '$charset'"
- }
-
- switch -exact -- $method {
- b -
- B {
- set method base64
- }
- q -
- Q {
- set method quoted-printable
- }
- default {
- error "unknown method '$method', must be B or Q"
- }
- }
-
- switch -exact -- $method {
- base64 {
- set result [base64 -mode decode -- $string]
- }
- quoted-printable {
- set result [qp_decode $string 1]
- }
- {} {
- # Go ahead
- }
- default {
- error "Can't handle content encoding \"$method\""
- }
- }
-
- return [list $enc $method $result]
-}
-
-# ::mime::field_decode --
-#
-# Word decodes strings that have been word encoded as per RFC 2047
-# and converts the string from the original encoding/charset to UTF.
-#
-# Arguments:
-# field The string to decode
-#
-# Results:
-# Returns the decoded string in UTF.
-
-proc ::mime::field_decode {field} {
- # ::mime::field_decode is broken. Here's a new version.
- # This code is in the public domain. Don Libes
-
- # Step through a field for mime-encoded words, building a new
- # version with unencoded equivalents.
-
- # Sorry about the grotesque regexp. Most of it is sensible. One
- # notable fudge: the final $ is needed because of an apparent bug
- # in the regexp engine where the preceding .* otherwise becomes
- # non-greedy - perhaps because of the earlier ".*?", sigh.
-
- while {[regexp {(.*?)(=\?(?:[^?]+)\?(?:.)\?(?:[^?]*)\?=)(.*)$} $field \
- ignore prefix encoded field]
- } {
- # don't allow whitespace between encoded words per RFC 2047
- if {{} ne $prefix} {
- if {![string is space $prefix]} {
- append result $prefix
- }
- }
-
- set decoded [word_decode $encoded]
- foreach {charset - string} $decoded break
-
- append result [::encoding convertfrom $charset $string]
- }
- append result $field
- return $result
-}
-
-## One-Shot Initialization
-
-::apply {{} {
- variable encList
- variable encAliasList
- variable reversemap
-
- foreach {enc mimeType} $encList {
- if {$mimeType eq {}} continue
- set reversemap([string tolower $mimeType]) $enc
- }
-
- foreach {enc mimeType} $encAliasList {
- set reversemap([string tolower $mimeType]) $enc
- }
-
- # Drop the helper variables
- unset encList encAliasList
-
-} ::mime}
-
-
-variable ::mime::internal 0
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/modpod-0.1.3.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/modpod-0.1.3.tm
deleted file mode 100644
index 540a1696..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/modpod-0.1.3.tm
+++ /dev/null
@@ -1,709 +0,0 @@
-# -*- tcl -*-
-# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt
-#
-# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
-# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# (C) 2024
-#
-# @@ Meta Begin
-# Application modpod 0.1.3
-# Meta platform tcl
-# Meta license
-# @@ Meta End
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# doctools header
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-#*** !doctools
-#[manpage_begin modpod_module_modpod 0 0.1.3]
-#[copyright "2024"]
-#[titledesc {Module API}] [comment {-- Name section and table of contents description --}]
-#[moddesc {-}] [comment {-- Description at end of page heading --}]
-#[require modpod]
-#[keywords module]
-#[description]
-#[para] -
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-#*** !doctools
-#[section Overview]
-#[para] overview of modpod
-#[subsection Concepts]
-#[para] -
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Requirements
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-#*** !doctools
-#[subsection dependencies]
-#[para] packages used by modpod
-#[list_begin itemized]
-
-package require Tcl 8.6-
-package require struct::set ;#review
-package require punk::lib
-package require punk::args
-#*** !doctools
-#[item] [package {Tcl 8.6-}]
-
-# #package require frobz
-# #*** !doctools
-# #[item] [package {frobz}]
-
-#*** !doctools
-#[list_end]
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-#*** !doctools
-#[section API]
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# oo::class namespace
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-namespace eval modpod::class {
- #*** !doctools
- #[subsection {Namespace modpod::class}]
- #[para] class definitions
- if {[info commands [namespace current]::interface_sample1] eq ""} {
- #*** !doctools
- #[list_begin enumerated]
-
- # oo::class create interface_sample1 {
- # #*** !doctools
- # #[enum] CLASS [class interface_sample1]
- # #[list_begin definitions]
-
- # method test {arg1} {
- # #*** !doctools
- # #[call class::interface_sample1 [method test] [arg arg1]]
- # #[para] test method
- # puts "test: $arg1"
- # }
-
- # #*** !doctools
- # #[list_end] [comment {-- end definitions interface_sample1}]
- # }
-
- #*** !doctools
- #[list_end] [comment {--- end class enumeration ---}]
- }
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# Base namespace
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-namespace eval modpod {
- namespace export {[a-z]*}; # Convention: export all lowercase
-
- variable connected
- if {![info exists connected(to)]} {
- set connected(to) list
- }
- variable modpodscript
- set modpodscript [info script]
- if {[string tolower [file extension $modpodscript]] eq ".tcl"} {
- set connected(self) [file dirname $modpodscript]
- } else {
- #expecting a .tm
- set connected(self) $modpodscript
- }
- variable loadables [info sharedlibextension]
- variable sourceables {.tcl .tk} ;# .tm ?
-
- #*** !doctools
- #[subsection {Namespace modpod}]
- #[para] Core API functions for modpod
- #[list_begin definitions]
-
-
-
- #proc sample1 {p1 args} {
- # #*** !doctools
- # #[call [fun sample1] [arg p1] [opt {?option value...?}]]
- # #[para]Description of sample1
- # return "ok"
- #}
-
- #old tar connect mechanism - review - not needed?
- proc connect {args} {
- puts stderr "modpod::connect--->>$args"
- set argd [punk::args::parse $args withdef {
- @id -id ::modpod::connect
- -type -default ""
- @values -min 1 -max 1
- path -type string -minsize 1 -help "path to .tm file or toplevel .tcl script within #modpod-- folder (unwrapped modpod)"
- }]
- catch {
- punk::lib::showdict $argd ;#heavy dependencies
- }
- set opt_path [dict get $argd values path]
- variable connected
- set original_connectpath $opt_path
- set modpodpath [modpod::system::normalize $opt_path] ;#
-
- if {$modpodpath in $connected(to)} {
- return [dict create ok ALREADY_CONNECTED]
- }
- lappend connected(to) $modpodpath
-
- set connected(connectpath,$opt_path) $original_connectpath
- set is_sourced [expr {[file normalize $modpodpath] eq [file normalize [info script]]}]
-
- set connected(location,$modpodpath) [file dirname $modpodpath]
- set connected(startdata,$modpodpath) -1
- set connected(type,$modpodpath) [dict get $argd opts -type]
- set connected(fh,$modpodpath) ""
-
- if {[string range [file tail $modpodpath] 0 7] eq "#modpod-"} {
- set connected(type,$modpodpath) "unwrapped"
- lassign [::split [file tail [file dirname $modpodpath]] -] connected(package,$modpodpath) connected(version,$modpodpath)
- set this_pkg_tm_folder [file dirname [file dirname $modpodpath]]
-
- } else {
- #connect to .tm but may still be unwrapped version available
- lassign [::split [file rootname [file tail $modpodpath]] -] connected(package,$modpodpath) connected(version,$modpodpath)
- set this_pkg_tm_folder [file dirname $modpodpath]
- if {$connected(type,$modpodpath) ne "unwrapped"} {
- #Not directly connected to unwrapped version - but may still be redirected there
- set unwrappedFolder [file join $connected(location,$modpodpath) #modpod-$connected(package,$modpodpath)-$connected(version,$modpodpath)]
- if {[file exists $unwrappedFolder]} {
- #folder with exact version-match must exist for redirect to 'unwrapped'
- set con(type,$modpodpath) "modpod-redirecting"
- }
- }
-
- }
- set unwrapped_tm_file [file join $this_pkg_tm_folder] "[set connected(package,$modpodpath)]-[set connected(version,$modpodpath)].tm"
- set connected(tmfile,$modpodpath)
- set tail_segments [list]
- set lcase_tmfile_segments [string tolower [file split $this_pkg_tm_folder]]
- set lcase_modulepaths [string tolower [tcl::tm::list]]
- foreach lc_mpath $lcase_modulepaths {
- set mpath_segments [file split $lc_mpath]
- if {[llength [struct::set intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} {
- set tail_segments [lrange [file split $this_pkg_tm_folder] [llength $mpath_segments] end]
- break
- }
- }
- if {[llength $tail_segments]} {
- set connected(fullpackage,$modpodpath) [join [concat $tail_segments [set connected(package,$modpodpath)]] ::] ;#full name of package as used in package require
- } else {
- set connected(fullpackage,$modpodpath) [set connected(package,$modpodpath)]
- }
-
- switch -exact -- $connected(type,$modpodpath) {
- "modpod-redirecting" {
- #redirect to the unwrapped version
- set loadscript_name [file join $unwrappedFolder #modpod-loadscript-$con(package,$modpod).tcl]
-
- }
- "unwrapped" {
- if {[info commands ::thread::id] ne ""} {
- set from [pid],[thread::id]
- } else {
- set from [pid]
- }
- #::modpod::Puts stderr "$from-> Package $connected(package,$modpodpath)-$connected(version,$modpodpath) is using unwrapped version: $modpodpath"
- return [list ok ""]
- }
- default {
- #autodetect .tm - zip/tar ?
- #todo - use vfs ?
-
- #connect to tarball - start at 1st header
- set connected(startdata,$modpodpath) 0
- set fh [open $modpodpath r]
- set connected(fh,$modpodpath) $fh
- fconfigure $fh -encoding iso8859-1 -translation binary -eofchar {}
-
- if {$connected(startdata,$modpodpath) >= 0} {
- #verify we have a valid tar header
- if {![catch {::modpod::system::tar::readHeader [read $fh 512]}]} {
- seek $fh $connected(startdata,$modpodpath) start
- return [list ok $fh]
- } else {
- #error "cannot verify tar header"
- #try zipfs
- if {[info commands tcl::zipfs::mount] ne ""} {
-
- }
- }
- }
- lpop connected(to) end
- set connected(startdata,$modpodpath) -1
- unset connected(fh,$modpodpath)
- catch {close $fh}
- return [dict create err {Does not appear to be a valid modpod}]
- }
- }
- }
- proc disconnect {{modpod ""}} {
- variable connected
- if {![llength $connected(to)]} {
- return 0
- }
- if {$modpod eq ""} {
- puts stderr "modpod::disconnect WARNING: modpod not explicitly specified. Disconnecting last connected: [lindex $connected(to) end]"
- set modpod [lindex $connected(to) end]
- }
-
- if {[set posn [lsearch $connected(to) $modpod]] == -1} {
- puts stderr "modpod::disconnect WARNING: disconnect called when not connected: $modpod"
- return 0
- }
- if {[string length $connected(fh,$modpod)]} {
- close $connected(fh,$modpod)
- }
- array unset connected *,$modpod
- set connected(to) [lreplace $connected(to) $posn $posn]
- return 1
- }
- proc get {args} {
- set argd [punk::args::parse $args withdef {
- @id -id ::modpod::get
- -from -default "" -help "path to pod"
- @values -min 1 -max 1
- filename
- }]
- set frompod [dict get $argd opts -from]
- set filename [dict get $argd values filename]
-
- variable connected
- #//review
- set modpod [::modpod::system::connect_if_not $frompod]
- set fh $connected(fh,$modpod)
- if {$connected(type,$modpod) eq "unwrapped"} {
- #for unwrapped connection - $connected(location) already points to the #modpod-pkg-ver folder
- if {[string range $filename 0 0 eq "/"]} {
- #absolute path (?)
- set path [file join $connected(location,$modpod) .. [string trim $filename /]]
- } else {
- #relative path - use #modpod-xxx as base
- set path [file join $connected(location,$modpod) $filename]
- }
- set fd [open $path r]
- #utf-8?
- #fconfigure $fd -encoding iso8859-1 -translation binary
- return [list ok [lindex [list [read $fd] [close $fd]] 0]]
- } else {
- #read from vfs
- puts stderr "get $filename from wrapped pod '$frompod' not implemented"
- }
- }
-
-
- #*** !doctools
- #[list_end] [comment {--- end definitions namespace modpod ---}]
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# Secondary API namespace
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-namespace eval modpod::lib {
- namespace export {[a-z]*}; # Convention: export all lowercase
- namespace path [namespace parent]
- #*** !doctools
- #[subsection {Namespace modpod::lib}]
- #[para] Secondary functions that are part of the API
- #[list_begin definitions]
-
- #proc utility1 {p1 args} {
- # #*** !doctools
- # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]]
- # #[para]Description of utility1
- # return 1
- #}
-
- proc is_valid_tm_version {versionpart} {
- #Needs to be suitable for use with Tcl's 'package vcompare'
- if {![catch [list package vcompare $versionparts $versionparts]]} {
- return 1
- } else {
- return 0
- }
- }
-
- #zipfile is a pure zip at this point - ie no script/exe header
- proc make_zip_modpod {args} {
- set argd [punk::args::parse $args withdef {
- @id -id ::modpod::lib::make_zip_modpod
- -offsettype -default "archive" -choices {archive file} -help\
- "Whether zip offsets are relative to start of file or start of zip-data within the file.
- 'archive' relative offsets are easier to work with (for writing/updating) in tools such as 7zip,peazip,
- but other tools may be easier with 'file' relative offsets. (e.g info-zip,pkzip)
- info-zip's 'zip -A' can sometimes convert archive-relative to file-relative.
- -offsettype archive is equivalent to plain 'cat prefixfile zipfile > modulefile'"
- @values -min 2 -max 2
- zipfile -type path -minsize 1 -help "path to plain zip file with subfolder #modpod-packagename-version containing .tm, data files and/or binaries"
- outfile -type path -minsize 1 -help "path to output file. Name should be of the form packagename-version.tm"
- }]
- set zipfile [dict get $argd values zipfile]
- set outfile [dict get $argd values outfile]
- set opt_offsettype [dict get $argd opts -offsettype]
-
-
- set mount_stub [string map [list %offsettype% $opt_offsettype] {
- #zip file with Tcl loader prepended. Requires either builtin zipfs, or vfs::zip to mount while zipped.
- #Alternatively unzip so that extracted #modpod-package-version folder is in same folder as .tm file.
- #generated using: modpod::lib::make_zip_modpod -offsettype %offsettype%
- if {[catch {file normalize [info script]} modfile]} {
- error "modpod zip stub error. Unable to determine module path. (possible safe interp restrictions?)"
- }
- if {$modfile eq "" || ![file exists $modfile]} {
- error "modpod zip stub error. Unable to determine module path"
- }
- set moddir [file dirname $modfile]
- set mod_and_ver [file rootname [file tail $modfile]]
- lassign [split $mod_and_ver -] moduletail version
- if {[file exists $moddir/#modpod-$mod_and_ver]} {
- source $moddir/#modpod-$mod_and_ver/$mod_and_ver.tm
- } else {
- #determine module namespace so we can mount appropriately
- proc intersect {A B} {
- if {[llength $A] == 0} {return {}}
- if {[llength $B] == 0} {return {}}
- if {[llength $B] > [llength $A]} {
- set res $A
- set A $B
- set B $res
- }
- set res {}
- foreach x $A {set ($x) {}}
- foreach x $B {
- if {[info exists ($x)]} {
- lappend res $x
- }
- }
- return $res
- }
- set lcase_tmfile_segments [string tolower [file split $moddir]]
- set lcase_modulepaths [string tolower [tcl::tm::list]]
- foreach lc_mpath $lcase_modulepaths {
- set mpath_segments [file split $lc_mpath]
- if {[llength [intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} {
- set tail_segments [lrange [file split $moddir] [llength $mpath_segments] end] ;#use properly cased tail
- break
- }
- }
- if {[llength $tail_segments]} {
- set fullpackage [join [concat $tail_segments $moduletail] ::] ;#full name of package as used in package require
- set mount_at #modpod/[file join {*}$tail_segments]/#mounted-modpod-$mod_and_ver
- } else {
- set fullpackage $moduletail
- set mount_at #modpod/#mounted-modpod-$mod_and_ver
- }
-
- if {[info commands tcl::zipfs::mount] ne ""} {
- #argument order changed to be consistent with vfs::zip::Mount etc
- #early versions: zipfs::Mount mountpoint zipname
- #since 2023-09: zipfs::Mount zipname mountpoint
- #don't use 'file exists' when testing mountpoints. (some versions at least give massive delays on windows platform for non-existance)
- #This is presumably related to // being interpreted as a network path
- set mountpoints [dict keys [tcl::zipfs::mount]]
- if {"//zipfs:/$mount_at" ni $mountpoints} {
- #despite API change tcl::zipfs package version was unfortunately not updated - so we don't know argument order without trying it
- if {[catch {
- #tcl::zipfs::mount $modfile //zipfs:/#mounted-modpod-$mod_and_ver ;#extremely slow if this is a wrong guess (artifact of aforementioned file exists issue ?)
- #puts "tcl::zipfs::mount $modfile $mount_at"
- tcl::zipfs::mount $modfile $mount_at
- } errM]} {
- #try old api
- if {![catch {tcl::zipfs::mount //zipfs:/$mount_at $modfile}]} {
- puts stderr "modpod stub>>> tcl::zipfs::mount failed.\nbut old api: tcl::zipfs::mount succeeded\n tcl::zipfs::mount //zipfs://$mount_at $modfile"
- puts stderr "Consider upgrading tcl runtime to one with fixed zipfs API"
- }
- }
- if {![file exists //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} {
- puts stderr "modpod stub>>> mount at //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm failed\n zipfs mounts: [zipfs mount]"
- #tcl::zipfs::unmount //zipfs:/$mount_at
- error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage"
- }
- }
- # #modpod-$mod_and_ver subdirectory always present in the archive so it can be conveniently extracted and run in that form
- source //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm
- } else {
- #fallback to slower vfs::zip
- #NB. We don't create the intermediate dirs - but the mount still works
- if {![file exists $moddir/$mount_at]} {
- if {[catch {package require vfs::zip} errM]} {
- set msg "Unable to load vfs::zip package to mount module $mod_and_ver (and zipfs not available either)"
- append msg \n "If neither zipfs or vfs::zip are available - the module can still be loaded by manually unzipping the file $modfile in place."
- append msg \n "The unzipped data will all be contained in a folder named #modpod-$mod_and_ver in the same parent folder as $modfile"
- error $msg
- } else {
- set fd [vfs::zip::Mount $modfile $moddir/$mount_at]
- if {![file exists $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} {
- vfs::zip::Unmount $fd $moddir/$mount_at
- error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage"
- }
- }
- }
- source $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm
- }
- }
- #zipped data follows
- }]
- #todo - test if supplied zipfile has #modpod-loadcript.tcl or some other script/executable before even creating?
- append mount_stub \x1A
- modpod::system::make_mountable_zip $zipfile $outfile $mount_stub $opt_offsettype
-
- }
-
- #*** !doctools
- #[list_end] [comment {--- end definitions namespace modpod::lib ---}]
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-#*** !doctools
-#[section Internal]
-namespace eval modpod::system {
- #*** !doctools
- #[subsection {Namespace modpod::system}]
- #[para] Internal functions that are not part of the API
-
- #deflate,store only supported
-
- #zipfile here is plain zip - no script/exe prefix part.
- proc make_mountable_zip {zipfile outfile mount_stub {offsettype "archive"}} {
- set inzip [open $zipfile r]
- fconfigure $inzip -encoding iso8859-1 -translation binary
- set out [open $outfile w+]
- fconfigure $out -encoding iso8859-1 -translation binary
- puts -nonewline $out $mount_stub
- set stuboffset [tell $out]
- lappend report "stub size: $stuboffset"
- fcopy $inzip $out
- close $inzip
-
- set size [tell $out]
- lappend report "modpod::system::make_mountable_zip"
- lappend report "tmfile : [file tail $outfile]"
- lappend report "output size : $size"
- lappend report "offsettype : $offsettype"
-
- if {$offsettype eq "file"} {
- #make zip offsets relative to start of whole file including prepended script.
- #same offset structure as Tcl's older 'zipfs mkimg' as at 2024-10
- #2025 - zipfs mkimg fixed to use 'archive' offset.
- #not editable by 7z,nanazip,peazip
-
- #we aren't adding any new files/folders so we can edit the offsets in place
-
- #Now seek in $out to find the end of directory signature:
- #The structure itself is 24 bytes Long, followed by a maximum of 64Kbytes text
- if {$size < 65559} {
- set tailsearch_start 0
- } else {
- set tailsearch_start [expr {$size - 65559}]
- }
- seek $out $tailsearch_start
- set data [read $out]
- #EOCD - End of Central Directory record
- #PK\5\6
- set start_of_end [string last "\x50\x4b\x05\x06" $data]
- #set start_of_end [expr {$start_of_end + $seek}]
- #incr start_of_end $seek
- set filerelative_eocd_posn [expr {$start_of_end + $tailsearch_start}]
-
- lappend report "kitfile-relative START-OF-EOCD: $filerelative_eocd_posn"
-
- seek $out $filerelative_eocd_posn
- set end_of_ctrl_dir [read $out]
- binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \
- eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len)
-
- lappend report "End of central directory: [array get eocd]"
- seek $out [expr {$filerelative_eocd_posn+16}]
-
- #adjust offset of start of central directory by the length of our sfx stub
- puts -nonewline $out [binary format i [expr {$eocd(diroffset) + $stuboffset}]]
- flush $out
-
- seek $out $filerelative_eocd_posn
- set end_of_ctrl_dir [read $out]
- binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \
- eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len)
-
- # 0x06054b50 - end of central dir signature
- puts stderr "$end_of_ctrl_dir"
- puts stderr "comment_len: $eocd(comment_len)"
- puts stderr "eocd sig: $eocd(signature) [punk::lib::dec2hex $eocd(signature)]"
- lappend report "New dir offset: $eocd(diroffset)"
- lappend report "Adjusting $eocd(totalnum) zip file items."
- catch {
- punk::lib::showdict -roottype list -chan stderr $report ;#heavy dependencies
- }
-
- seek $out $eocd(diroffset)
- for {set i 0} {$i <$eocd(totalnum)} {incr i} {
- set current_file [tell $out]
- set fileheader [read $out 46]
- puts --------------
- puts [ansistring VIEW -lf 1 $fileheader]
- puts --------------
- #binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \
- # x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset)
-
- binary scan $fileheader ic4sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \
- x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset)
- set ::last_header $fileheader
-
- puts "sig: $x(sig) (hex: [punk::lib::dec2hex $x(sig)])"
- puts "ver: $x(version)"
- puts "method: $x(method)"
-
- #PK\1\2
- #33639248 dec = 0x02014b50 - central directory file header signature
- if { $x(sig) != 33639248 } {
- error "modpod::system::make_mountable_zip Bad file header signature at item $i: dec:$x(sig) hex:[punk::lib::dec2hex $x(sig)]"
- }
-
- foreach size $x(lengths) var {filename extrafield comment} {
- if { $size > 0 } {
- set x($var) [read $out $size]
- } else {
- set x($var) ""
- }
- }
- set next_file [tell $out]
- lappend report "file $i: $x(offset) $x(sizes) $x(filename)"
-
- seek $out [expr {$current_file+42}]
- puts -nonewline $out [binary format i [expr {$x(offset)+$stuboffset}]]
-
- #verify:
- flush $out
- seek $out $current_file
- set fileheader [read $out 46]
- lappend report "old $x(offset) + $stuboffset"
- binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \
- x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset)
- lappend report "new $x(offset)"
-
- seek $out $next_file
- }
- }
-
- close $out
- #pdict/showdict reuire punk & textlib - ie lots of dependencies
- #don't fall over just because of that
- catch {
- punk::lib::showdict -roottype list -chan stderr $report
- }
- #puts [join $report \n]
- return
- }
-
- proc connect_if_not {{podpath ""}} {
- upvar ::modpod::connected connected
- set podpath [::modpod::system::normalize $podpath]
- set docon 0
- if {![llength $connected(to)]} {
- if {![string length $podpath]} {
- error "modpod::system::connect_if_not - Not connected to a modpod file, and no podpath specified"
- } else {
- set docon 1
- }
- } else {
- if {![string length $podpath]} {
- set podpath [lindex $connected(to) end]
- puts stderr "modpod::system::connect_if_not WARNING: using last connected modpod:$podpath for operation\n -podpath not explicitly specified during operation: [info level -1]"
- } else {
- if {$podpath ni $connected(to)} {
- set docon 1
- }
- }
- }
- if {$docon} {
- if {[lindex [modpod::connect $podpath]] 0] ne "ok"} {
- error "modpod::system::connect_if_not error. file $podpath does not seem to be a valid modpod"
- } else {
- return $podpath
- }
- }
- #we were already connected
- return $podpath
- }
-
- proc myversion {} {
- upvar ::modpod::connected connected
- set script [info script]
- if {![string length $script]} {
- error "No result from \[info script\] - modpod::system::myversion should only be called from within a loading modpod"
- }
- set fname [file tail [file rootname [file normalize $script]]]
- set scriptdir [file dirname $script]
-
- if {![string match "#modpod-*" $fname]} {
- lassign [lrange [split $fname -] end-1 end] _pkgname version
- } else {
- lassign [scan [file tail [file rootname $script]] {#modpod-loadscript-%[a-z]-%s}] _pkgname version
- if {![string length $version]} {
- #try again on the name of the containing folder
- lassign [scan [file tail $scriptdir] {#modpod-%[a-z]-%s}] _pkgname version
- #todo - proper walk up the directory tree
- if {![string length $version]} {
- #try again on the grandparent folder (this is a standard depth for sourced .tcl files in a modpod)
- lassign [scan [file tail [file dirname $scriptdir]] {#modpod-%[a-z]-%s}] _pkgname version
- }
- }
- }
-
- #tarjar::Log debug "'myversion' determined version for [info script]: $version"
- return $version
- }
-
- proc myname {} {
- upvar ::modpod::connected connected
- set script [info script]
- if {![string length $script]} {
- error "No result from \[info script\] - modpod::system::myname should only be called from within a loading modpod"
- }
- return $connected(fullpackage,$script)
- }
- proc myfullname {} {
- upvar ::modpod::connected connected
- set script [info script]
- #set script [::tarjar::normalize $script]
- set script [file normalize $script]
- if {![string length $script]} {
- error "No result from \[info script\] - modpod::system::myfullname should only be called from within a loading tarjar"
- }
- return $::tarjar::connected(fullpackage,$script)
- }
- proc normalize {path} {
- #newer versions of Tcl don't do tilde sub
-
- #Tcl's 'file normalize' seems to do some unfortunate tilde substitution on windows.. (at least for relative paths)
- # we take the assumption here that if Tcl's tilde substitution is required - it should be done before the path is provided to this function.
- set matilda "<_tarjar_tilde_placeholder_>" ;#token that is *unlikely* to occur in the wild, and is somewhat self describing in case it somehow ..escapes..
- set path [string map [list ~ $matilda] $path] ;#give our tildes to matilda to look after
- set path [file normalize $path]
- #set path [string tolower $path] ;#must do this after file normalize
- return [string map [list $matilda ~] $path] ;#get our tildes back.
-}
-}
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Ready
-package provide modpod [namespace eval modpod {
- variable pkg modpod
- variable version
- set version 0.1.3
-}]
-return
-
-#*** !doctools
-#[manpage_end]
-
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/natsort-0.1.1.6.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/natsort-0.1.1.6.tm
deleted file mode 100644
index 07c29895..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/natsort-0.1.1.6.tm
+++ /dev/null
@@ -1,1962 +0,0 @@
-#! /usr/bin/env tclsh
-
-
-#todo - remove flagfilter - use punk::args?
-package require flagfilter
-namespace import ::flagfilter::check_flags
-
-namespace eval natsort {
- #REVIEW - determine and document the purpose of scriptdir being added to tm path
- proc scriptdir {} {
- set possibly_linked_script [file dirname [file normalize [file join [info script] __dummy__]]]
- if {[file isdirectory $possibly_linked_script]} {
- return $possibly_linked_script
- } else {
- return [file dirname $possibly_linked_script]
- }
- }
- if {![interp issafe]} {
- set sdir [scriptdir]
- #puts stderr "natsort tcl::tm::add $sdir"
- if {$sdir ni [tcl::tm::list]} {
- catch {tcl::tm::add $sdir}
- }
- }
-}
-
-
-namespace eval natsort {
- variable stacktrace_on 0
-
- proc do_error {msg {then error}} {
- #note we exit or error out even if debug selected - as every do_error call is meant to interrupt code processing at the site of call
- #this is not just a 'logging' call even though it has log-like descriptors
- lassign $then type code
- if {$code eq ""} {
- set code 1
- }
- set type [string tolower $type]
- set levels [list debug info notice warn error critical]
- if {$type in [concat $levels exit]} {
- puts stderr "|$type> $msg"
- } else {
- puts stderr "|>natsort_call_err> unable to interpret 2nd argument to do_error: '$then' should be one of '$levels' or 'exit '"
- }
- flush stderr
- if {$::tcl_interactive} {
- #may not always be desirable - but assumed to be more useful not to exit despite request, to aid in debugging
- if {[string tolower $type] eq "exit"} {
- puts stderr " (exit suppressed due to tcl_interactive - raising error instead)"
- if {![string is digit -strict $code]} {
- puts stderr "|>natsort_call_err> unable to interpret 2nd argument to do_error: '$then' should be: 'exit '"
- }
- flush stderr
- }
- return -code error $msg
- } else {
- if {$type ne "exit"} {
- return -code error $msg
- } else {
- if {[string is digit -strict $code]} {
- exit $code
- } else {
- puts stderr "|>natsort_call_err> unable to interpret 2nd argument to do_error: '$then' should be 'error' or 'exit '"
- return -code error $msg
- }
- }
- }
- }
-
-
-
-
-
-
- variable debug 0
- variable testlist
- set testlist {
- 00.test-firstposition.txt
- 0001.blah.txt
- 1.test-sorts-after-all-leadingzero-number-one-equivs.txt
- 1010.thousand-and-ten.second.txt
- 01010.thousand-and-ten.first.txt
- 0001.aaa.txt
- 001.zzz.txt
- 08.octal.txt-last-octal
- 008.another-octal-first-octal.txt
- 08.again-second-octal.txt
- 001.a.txt
- 0010.reconfig.txt
- 010.etc.txt
- 005.etc.01.txt
- 005.Etc.02.txt
- 005.123.abc.txt
- 200.somewhere.txt
- 2zzzz.before-somewhere.txt
- 00222-after-somewhere.txt
- 005.00010.abc.txt
- 005.a3423bc.00010.abc.txt
- 005.001.abc.txt
- 005.etc.1010.txt
- 005.etc.010.txt
- 005.etc.10.txt
- " 005.etc.10.txt"
- 005.etc.001.txt
- 20.somewhere.txt
- 4611686018427387904999999999-bignum.txt
- 4611686018427387903-bigishnum.txt
- 9223372036854775807-bigint.txt
- etca-a
- etc-a
- etc2-a
- a0001blah.txt
- a010.txt
- winlike-sort-difference-0.1.txt
- winlike-sort-difference-0.1.1.txt
- a1.txt
- b1-a0001blah.txt
- b1-a010.txt
- b1-a1.txt
- -a1.txt
- --a1.txt
- --a10.txt
- 2.high-two.yml
- 02.higher-two.yml
- reconfig.txt
- _common.stuff.txt
- CASETEST.txt
- casetest.txt
- something.txt
- some~thing.txt
- someathing.txt
- someThing.txt
- thing.txt
- thing_revised.txt
- thing-revised.txt
- "thing revised.txt"
- "spacetest.txt"
- " spacetest.txt"
- " spacetest.txt"
- "spacetest2.txt"
- "spacetest 2.txt"
- "spacetest02.txt"
- name.txt
- name2.txt
- "name .txt"
- "name2 .txt"
- blah.txt
- combined.txt
- a001.txt
- .test
- .ssh
- "Feb 10.txt"
- "Feb 8.txt"
- 1ab23v23v3r89ad8a8a8a9d.txt
- "Folder (10)/file.tar.gz"
- "Folder/file.tar.gz"
- "Folder (1)/file (1).tar.gz"
- "Folder (1)/file.tar.gz"
- "Folder (01)/file.tar.gz"
- "Folder1/file.tar.gz"
- "Folder(1)/file.tar.gz"
-
- }
- lappend testlist "Some file.txt"
- lappend testlist " Some extra file1.txt"
- lappend testlist " Some extra file01.txt"
- lappend testlist " some extra file1.txt"
- lappend testlist " Some extra file003.txt"
- lappend testlist " Some file.txt"
- lappend testlist "Some extra file02.txt"
- lappend testlist "Program Files (x86)"
- lappend testlist "01999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999991-bigger-pathologically-bignum.txt"
- lappend testlist "199999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999-pathologically-bignum.txt"
- lappend testlist "29999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999-smaller-pathologically-bignum.txt"
- lappend testlist "199999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999-pathologically-bignum.txt with (more 1.txt"
- lappend testlist "199999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999-pathologically-bignum.txt with (more 01.txt"
- lappend testlist "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1.pathological-num-nonnum-swapping-leadzero-should-be-first.txt"
- lappend testlist "a1a1a1a1a1a1a1a1a1a1a1a01a1a1a1a1a1a1a1a1a1a1a1a1.pathological-num-nonnum-swapping-leadzero-should-be-first.txt"
- lappend testlist "b1b1b1b1.txt"
- lappend testlist "b1b01z1z1.txt"
- lappend testlist "c1c111c1.txt"
- lappend testlist "c1c1c1c1.txt"
-
- namespace eval overtype {
- proc right {args} {
- # @d !todo - implement overflow, length checks etc
-
- if {[llength $args] < 2} {
- error {usage: ?-overflow [1|0]? undertext overtext}
- }
- foreach {undertext overtext} [lrange $args end-1 end] break
-
- set opt(-overflow) 0
- array set opt [lrange $args 0 end-2]
-
-
- set olen [string length $overtext]
- set ulen [string length $undertext]
-
- if {$opt(-overflow)} {
- return [string range $undertext 0 end-$olen]$overtext
- } else {
- if {$olen > $ulen} {
- set diff [expr {$olen - $ulen}]
- return [string range $undertext 0 end-$olen][string range $overtext 0 end-$diff]
- } else {
- return [string range $undertext 0 end-$olen]$overtext
- }
- }
- }
- proc left {args} {
- # @c overtype starting at left (overstrike)
- # @c can/should we use something like this?: 'format "%-*s" $len $overtext
-
- if {[llength $args] < 2} {
- error {usage: ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext}
- }
- foreach {undertext overtext} [lrange $args end-1 end] break
-
- set opt(-ellipsis) 0
- set opt(-ellipsistext) {...}
- set opt(-overflow) 0
- array set opt [lrange $args 0 end-2]
-
-
- set len [string length $undertext]
- set overlen [string length $overtext]
- set diff [expr {$overlen - $len}]
-
- #puts stdout "====================>overtype: datalen:$len overlen:$overlen diff:$diff"
- #puts stdout "====================>overtype: data: $overtext"
- if {$diff > 0} {
- if {$opt(-overflow)} {
- return $overtext
- } else {
- if {$opt(-ellipsis)} {
- return [overtype::left [string range $overtext 0 [expr {$len -1}]] $opt(-ellipsistext)]
- } else {
- return [string range $overtext 0 [expr {$len -1}]]
- }
- }
- } else {
- return "$overtext[string range $undertext $overlen end]"
- }
- }
-
- }
-
- #considered using hex to make large numbers more compact for viewing in debug output - but it's not that much shorter and probably obscures more than it helps.
- proc hex2dec {largeHex} {
- #todo - use punk::lib::hex2dec - (scan supports ll so can do larger hex values directly)
- set res 0
- set largeHex [string map {_ {}} $largeHex]
- if {[string length $largeHex] <=7} {
- #scan can process up to FFFFFFF and does so quickly
- return [scan $largeHex %x]
- }
- foreach hexDigit [split $largeHex {}] {
- set new 0x$hexDigit
- set res [expr {16*$res + $new}]
- }
- return $res
- }
- proc dec2hex {decimalNumber} {
- format %4.4llX $decimalNumber
- }
-
- #punk::lib::trimzero
- proc trimzero {number} {
- set trimmed [string trimleft $number 0]
- if {[string length $trimmed] == 0} {
- set trimmed 0
- }
- return $trimmed
- }
- #todo - consider human numeric split
- #e.g consider SI suffixes k|KMGTPEZY in that order
-
- #in this context, for natural sorting - numeric segments don't contain underscores or other punctuation such as . - + etc.
- #review - what about unicode equivalents such as wide numerals \UFF10 to \UFF19? unicode normalization?
- proc split_numeric_segments {name} {
- set segments [list]
- while {[string length $name]} {
- if {[scan $name {%[0-9]%n} chunk len] == 2} {
- lappend segments $chunk
- set name [string range $name $len end]
- }
- if {[scan $name {%[^0-9]%n} chunk len] == 2} {
- lappend segments $chunk
- set name [string range $name $len end]
- }
- }
- return $segments
- }
-
- proc padleft {str count {ch " "}} {
- set val [string repeat $ch $count]
- append val $str
- set diff [expr {max(0,$count - [string length $str])}]
- set offset [expr {max(0,$count - $diff)}]
- set val [string range $val $offset end]
- }
-
-
- # Sqlite may have limited collation sequences available in default builds.
- # with custom builds - there may be others such as 'natsort' - see https://sqlite.org/forum/forumpost/e4dc6f3331
- # This is of limited use with the few builtin collations available in 2023 ie binary,nocase & rtrim
- # but may provide a quicker,flexible sort option, especially if/when more collation sequences are added to sqlite
- # There are also prebuilt packages such as sqlite3-icu which allows things like "SELECT icu_load_collation('en_AU', 'australian');"
- proc sort_sqlite {stringlist args} {
- package require sqlite3
-
-
- set args [check_flags -caller natsort_sqlite -defaults [list -db :memory: -collate nocase -winlike 0 -topchars "\uFFFF" -debug 0 -splitchars [list / . - _] -extras {all}] -values $args]
- set db [string trim [dict get $args -db]]
- set collate [string trim [dict get $args -collate]]
- set debug [string trim [dict get $args -debug]]
- set topchars [string trim [dict get $args -topchars]]
-
- set topdot [expr {"." in $topchars}]
- set topunderscore [expr {"_" in $topchars}]
-
-
- sqlite3 db_sort_basic $db
- set orderedlist [list]
- db_sort_basic eval [string map [list %collate% $collate] {create table sqlitesort(index0 text COLLATE %collate%, name text COLLATE %collate%)}]
- foreach nm $stringlist {
- set segments [split_numeric_segments $nm]
- set index ""
- set s 0
- foreach seg $segments {
- if {($s == 0) && ![string length [string trim $seg]]} {
- #don't index leading space
- } elseif {($s == 0) && ($topunderscore) && [string match _* [string trim $seg]]} {
- append index "[padleft "0" 5]-d -100 topunderscore "
- append index [string trim $seg]
- } elseif {($s == 0) && ($topdot) && [string match .* [string trim $seg]]} {
- append index "[padleft "0" 5]-d -50 topdot "
- append index [string trim $seg]
- } else {
- if {[string is digit [string trim $seg]]} {
- set basenum [trimzero [string trim $seg]]
- set lengthindex "[padleft [string length $basenum] 5]-d"
- append index "$lengthindex "
- #append index [padleft $basenum 40]
- append index $basenum
- } else {
- append index [string trim $seg]
- }
- }
- incr s
- }
- puts stdout ">>$index"
- db_sort_basic eval {insert into sqlitesort values($index,$nm)}
- }
- db_sort_basic eval [string map [list %collate% $collate] {select name from sqlitesort order by index0 COLLATE %collate% ASC, name COLLATE %collate% ASC }] {
- lappend orderedlist $name
- }
- db_sort_basic close
- return $orderedlist
- }
-
- proc get_leading_char_count {str char} {
- #todo - something more elegant? regex?
- set count 0
- foreach c [split $str "" ] {
- if {$c eq $char} {
- incr count
- } else {
- break
- }
- }
- return $count
- }
- proc stacktrace {} {
- set stack "Stack trace:\n"
- for {set i 1} {$i < [info level]} {incr i} {
- set lvl [info level -$i]
- set pname [lindex $lvl 0]
- append stack [string repeat " " $i]$pname
-
- if {![catch {info args $pname} pargs]} {
- foreach value [lrange $lvl 1 end] arg $pargs {
-
- if {$value eq ""} {
- if {$arg != 0} {
- info default $pname $arg value
- }
- }
- append stack " $arg='$value'"
- }
- } else {
- append stack " !unknown vars for $pname"
- }
-
- append stack \n
- }
- return $stack
- }
-
- proc get_char_count {str char} {
- #faster than lsearch on split for str of a few K
- expr {[tcl::string::length $str]-[tcl::string::length [tcl::string::map "$char {}" $str]]}
- }
-
- proc build_key {chunk splitchars topdict tagconfig debug} {
- variable stacktrace_on
- if {$stacktrace_on} {
- puts stderr "+++>[stacktrace]"
- }
-
- set index_map [list - "" _ ""]
- #e.g - need to maintain the order
- #a b.txt
- #a book.txt
- #ab.txt
- #abacus.txt
-
-
- set original_splitchars [dict get $tagconfig original_splitchars]
-
- # tag_dashes test moved from loop - review
- set tag_dashes 0
- if {![string length [dict get $tagconfig last_part_text_tag]]} {
- #winlike
- set tag_dashes 1
- }
- if {("-" ni $original_splitchars)} {
- set tag_dashes 1
- }
- if {$debug >= 3} {
- puts stdout "START build_key chunk : $chunk"
- puts stdout "START build_key splitchars : $splitchars $topdict $tagconfig NO tag dashes"
- }
-
-
- ## index_map will have no effect if we've already split on the char anyway(?)
- #foreach m [dict keys $index_map] {
- # if {$m in $original_splitchars} {
- # dict unset index_map $m
- # }
- #}
-
- #if {![string length $chunk]} return
-
- set result ""
- if {![llength $splitchars]} {
- #no more structural splits - but we need to examine numeric/non-numeric segments at the lowest level.
- # we are at a leaf in the recursive split hierarchy
-
- set s "" ;#we never actually split on "" (unless that was put in splitchars.. but it probably shouldn't be)
- set parts [list $chunk] ;#important to treat as list or leading/trailing whitespace lost
-
-
- } else {
- set s [lindex $splitchars 0]
- if {"spudbucket$s" in "[split $chunk {}]"} {
- error "dead-branch spudbucket"
- set partindex [build_key $chunk [lrange $splitchars 1 end] $topdict $tagconfig $debug]
- if {[dict get $tagconfig showsplits]} {
- set pfx "(1${s}=)" ;# = sorts before _
- set partindex ${pfx}$partindex
- }
-
- return $partindex
- } else {
- set parts_below_index ""
-
- if {$s ni [split $chunk ""]} {
- #$s can be an empty string
- set parts [list $chunk]
- } else {
- set parts [split $chunk $s] ;#whitespace preserved - even if splitting on s that is not in string.
- }
- #assert - we have a splitchar $s that is in the chunk - so at least one part
- if {(![string length $s] || [llength $parts] == 0)} {
- error "buld_key assertion false empty split char and/or no parts"
- }
-
- set pnum 1 ;# 1 based for clarity of reading index in debug output
- set subpart_count [llength $parts]
-
- set sub_splits [lrange $splitchars 1 end] ;#pass same splitchars to each subpart
- foreach p $parts {
- set partindex [build_key $p $sub_splits $topdict $tagconfig $debug]
- set lastpart [expr {$pnum == $subpart_count}]
-
-
- #######################
- set showsplits [dict get $tagconfig showsplits]
- #split prefixing experiment - maybe not suitable for general use - as it affects sort order
- #note that pfx must be consistent until last one, no matter how many partnumbers there are in total.
- # we don't want to influence sort order before reaching end.
- #e.g for:
- #(1.=)...
- #(1._)...(2._)...(3.=)
- #(1._)...(2.=)
- #Note that this is probably more suitable for highly structure dependant sorts where the results are maybe less.. natural.
- if {$showsplits} {
- if {$lastpart} {
- set pfx "(${pnum}${s}_"
- #set pfx "(${pnum}${s}=)" ;# = sorts before _
- } else {
- set pfx "(${pnum}${s}_"
- }
- append parts_below_index $pfx
- }
- #######################
-
- if {$lastpart} {
- if {[string length $p] && [string is digit $p]} {
- set last_part_tag "<22${s}>"
- } else {
- set last_part_tag "<33${s}>"
- }
-
- set last_part_text_tag [dict get $tagconfig last_part_text_tag]
- #for -winlike 1 there is no tag configured. Windows explorer likes to put things in the order:
- # module-0.1.1.tm
- # module-0.1.1.2.tm
- # module-0.1.tm
- # arguably -winlike 0 is more natural/human
- # module-0.1.tm
- # module-0.1.1.tm
- # module-0.1.1.2.tm
-
- if {[string length $last_part_text_tag]} {
- #replace only the first text-tag (<30>) from the subpart_index
- if {[string match "<30?>*" $partindex]} {
- #give textual string index a specific tag for last part in split only. e.g <130> for lower than integers
- set partindex "<130>[string range $partindex 5 end]"
- }
- #append parts_below_index $last_part_tag
- }
- #set partindex $last_part_tag$partindex
-
-
- }
- append parts_below_index $partindex
-
-
-
- if {$showsplits} {
- if {$lastpart} {
- set suffix "${pnum}${s}=)" ;# = sorts before _
- } else {
- set suffix "${pnum}${s}_)"
- }
- append parts_below_index $suffix
- }
-
-
- incr pnum
- }
- append parts_below_index "" ;# don't add anything at the tail that may perturb sort order
-
- if {$debug >= 3} {
- set pad [string repeat " " 20]
- puts stdout "END build_key chunk : $chunk "
- puts stdout "END build_key splitchars : $splitchars $topdict $tagconfig NO tag dashes"
- puts stdout "END build_key ret below_index: $parts_below_index"
- }
- return $parts_below_index
-
-
- }
- }
-
-
-
- #puts stdout ">>>chunk:'$chunk'<<< split-on:$s parts: '$parts' splitchars: $splitchars -topdict:$topdict"
-
-
-
-
-
- #if {$chunk eq ""} {
- # puts "___________________________________________!!!____"
- #}
- #puts stdout "-->chunk:$chunk $s parts:$parts"
-
- #puts stdout "---chunk:'$chunk' part:'$part' parts:'$parts' s:'$s'"
-
-
-
-
- set segments [split_numeric_segments $chunk] ;#!
- set stringindex ""
- set segnum 0
- foreach seg $segments {
- #puts stdout "=================---->seg:$seg segments:$segments"
- #-strict ?
- if {[string length $seg] && [string is digit $seg]} {
- set basenum [trimzero [string trim $seg]]
- set lengthindex "[padleft [string length $basenum] 4]d"
- #append stringindex "<20>$lengthindex $basenum $seg"
- } else {
- set c1 [string range $seg 0 0]
- #puts stdout "==============> c1'$c1' topdict: $topdict stringindex:$stringindex"
-
- if {$c1 in [dict keys $topdict]} {
- set tag [dict get $topdict $c1]
- #append stringindex "${tag}$c1"
- #set seg [string range $seg 1 end]
- }
- #textindex
- set leader "<30>"
- set idx $seg
- set idx [string trim $idx]
- set idx [string tolower $idx]
- set idx [string map $index_map $idx]
-
-
-
-
-
- #set the X-c count to match the length of the index - not the raw data
- set lengthindex "[padleft [string length $idx] 4]c"
-
- #append stringindex "${leader}$idx $lengthindex $texttail"
- }
- }
-
- if {[llength $parts] != 1} {
- error "build_key assertion fail llength parts != 1 parts:$parts"
- }
-
- set segtail_clearance_buffer " " ;#space to clear other split indicators if using showsplits
- set segtail $segtail_clearance_buffer
- append segtail "\["
- set grouping ""
- set pnum 0
- foreach p $parts {
- set sublen_list [list]
- set subsegments [split_numeric_segments $p]
- set i 0
-
- set partsorter ""
- foreach sub $subsegments {
- ##don't trim spaces here - it would be inconsistent. Some subs are pure whitespace - others have internal whitespace. e.g "a4 400b a b2" becomes "a 4 { } 400 {b a b} 2"
- #mapping away all whitespace would be consistent, but not necessarily desirable. If it's in the index_map it'll happen anyway - so we don't do it manually here except for evaluating conditions.
- set test_trim [string trim $sub]
- set str $sub
- set str [string tolower $str]
- set str [string map $index_map $str]
- if {[string length $test_trim] && [string is digit $test_trim]} {
- append partsorter [trimzero $str]
- } else {
- append partsorter "$str"
- }
- append partsorter
- }
-
-
- foreach sub $subsegments {
-
- if {[string length $sub] && [string is digit $sub]} {
- set basenum [trimzero [string trim $sub]]
- set subequivs $basenum
- set lengthindex "[padleft [string length $subequivs] 4]d "
- set idx "$lengthindex [padleft $basenum 10]" ;#todo - cycle through data and determine longest
- set tail [overtype::left [string repeat " " 10] $sub]
- #set tail ""
- } else {
- set idx ""
-
-
- set lookahead [lindex $subsegments $i+1]
- if {![string length $lookahead]} {
- set zeronum "[padleft 0 4]d0"
- } else {
- set zeronum ""
- }
- set subequivs $sub
- #set subequivs [string trim $subequivs]
- set subequivs [string tolower $subequivs]
- set subequivs [string map $index_map $subequivs]
-
- append idx $subequivs
- append idx $zeronum
-
- set idx $subequivs
-
-
- #
-
- set ch "-"
- if {$tag_dashes} {
- #puts stdout "____TAG DASHES"
- #winlike
- set numleading [get_leading_char_count $seg $ch]
- if {$numleading > 0} {
- set texttail "<31-leading[padleft $numleading 4]$ch>"
- } else {
- set texttail "<30>"
- }
- set numothers [expr {[get_char_count $seg $ch] - $numleading}]
- if {$debug >= 2} {
- puts stdout "____dashcount: [get_char_count $seg $ch] numothers: $numothers"
- }
- if {$numothers > 0} {
- append texttail "<31-others[padleft $numothers 4]$ch>"
- } else {
- append textail "<30>"
- }
- } else {
- set texttail "<30>"
- }
-
-
-
-
- #set idx $partsorter
- set tail ""
- #set tail [string tolower $sub] ;#raw
- #set tail $partsorter
- #append tail ":[string tolower $p]" ;#we need the whole part - even though it makes the index much larger. !todo - tagconfig switch to toggle case sensitive sorting
- }
-
- append grouping "$idx $tail|$s"
- incr i
- }
-
-
-
-
-
- if {$p eq ""} {
- # no subsegments..
- set zeronum "[padleft 0 4]d0"
- #append grouping "\u000$zerotail"
- append grouping ".$zeronum"
- }
-
- #append grouping |
- #append grouping $s
- #foreach len $sublen_list {
- # append segtail "<[padleft $len 3]>"
- #}
- incr pnum
- }
- set grouping [string trimright $grouping $s]
- append grouping "[padleft [llength $parts] 4]"
- append segtail $grouping
-
-
- #append segtail " <[padleft [llength $parts] 4]>"
-
- append segtail "\]"
-
-
- #if {[string length $seg] && [string is digit $seg]} {
- # append segtail "<20>"
- #} else {
- # append segtail "<30>"
- #}
- append stringindex $segtail
-
- incr segnum
-
-
-
-
- lappend indices $stringindex
-
- if {[llength $indices] > 1} {
- puts stderr "INDICES [llength $indices]: $stringindex"
- error "build_key assertion error deadconcept indices"
- }
-
- #topchar handling on splitter characters
- #set c1 [string range $chunk 0 0]
- if {$s in [dict keys $topdict]} {
- set tag [dict get $topdict $s]
- set joiner [string map [list ">" "$s>"] ${tag}]
- #we have split on this character $s so if the first part is empty string then $s was a leading character
- # we need to bring a tag out front for this, or it will be dominated by the leading sections-remaing tag
- # (since the empty string produces no tag of it's own - ?)
- if {[string length [lindex $parts 0]] == 0} {
- set prefix ${joiner}
- } else {
- set prefix ""
- }
- } else {
- #use standard character-data positioning tag if no override from topdict
- set joiner "<30J>$s"
- set prefix ""
- }
-
-
- set contentindex $prefix[join $indices $joiner]
- if {[string length $s]} {
- set split_indicator ""
- } else {
- set split_indicator ""
-
- }
- if {![string length $s]} {
- set s ~
- }
-
- #return "[overtype::left [string repeat { } 80] $contentindex][overtype::left [string repeat { } 10] [list $s $chunk]]"
- #return $contentindex$split_indicator
- #return [overtype::left [string repeat - 40] $contentindex]
-
- if {$debug >= 3} {
- puts stdout "END build_key chunk : $chunk"
- puts stdout "END build_key splitchars : $splitchars $topdict $tagconfig NO tag dashes"
- puts stdout "END build_key ret contentidx : $contentindex"
- }
- return $contentindex
- }
-
- #----------------------------------------
- #line-processors - data always last argument - opts can be empty string
- #all processor should accept empty opts and ignore opts if they don't use them
- proc _lineinput_as_tcl1 {opts line} {
- set out ""
- foreach i $line {
- append out "$i "
- }
- set out [string range $out 0 end-1]
- return $out
- }
- #should be equivalent to above
- proc _lineinput_as_tcl {opts line} {
- return [concat {*}$line]
- }
- #will put extra tcl quoting if it was already tcl-shaped e.g text "a" -> {"a"}
- proc _lineoutput_as_tcl {opts line} {
- return [regexp -inline -all {\S+} $line]
- }
-
- proc _lineinput_as_raw {opts line} {
- return $line
- }
- proc _lineoutput_as_raw {opts line} {
- return $line
- }
-
- #words is opposite of tcl
- proc _lineinput_as_words {opts line} {
- #wordlike_parts
- return [regexp -inline -all {\S+} $line]
- }
- proc _lineoutput_as_words {opts line} {
- return [concat {*}$line]
- }
-
- #opts same as tcllib csv::split - except without the 'line' element
- #?-alternate? ?sepChar? ?delChar?
- proc _lineinput_as_csv {opts line} {
- package require csv
- if {[lindex $opts 0] eq "-alternate"} {
- return [csv::split -alternate $line {*}[lrange $opts 1 end]]
- } else {
- return [csv::split $line {*}$opts]
- }
- }
- #opts same as tcllib csv::join
- #?sepChar? ?delChar? ?delMode?
- proc _lineoutput_as_csv {opts line} {
- package require csv
- return [csv::join $line {*}$opts]
- }
- #----------------------------------------
- variable sort_flagspecs
- set sort_flagspecs [dict create\
- -caller natsort::sort \
- -return supplied|defaults \
- -defaults [list -collate nocase \
- -winlike 0 \
- -splits "\uFFFF" \
- -topchars {. _} \
- -showsplits 1 \
- -sortmethod ascii \
- -collate "\uFFFF" \
- -inputformat raw \
- -inputformatapply {index data} \
- -inputformatoptions "" \
- -outputformat raw \
- -outputformatoptions "" \
- -cols "\uFFFF" \
- -debug 0 -db "" -stacktrace 0 -splits "\uFFFF" -showsplits 0] \
- -required {all} \
- -extras {none} \
- -commandprocessors {}\
- ]
-
- proc sort {stringlist args} {
- #puts stdout "natsort::sort args: $args"
- variable debug
- variable sort_flagspecs
- if {![llength $stringlist]} return
- if {[llength $stringlist] == 1} {
- if {"-inputformat" ni $args && "-outputformat" ni $args} {
- return $stringlist
- }
- }
-
- #allow pass through of the check_flags flag -debugargs so it can be set by the caller
- set debugargs 0
- if {[set posn [lsearch $args -debugargs]] >=0} {
- if {$posn == [llength $args]-1} {
- #-debugargs at tail of list
- set debugargs 1
- } else {
- set debugargs [lindex $args $posn+1]
- }
- }
-
- #-return flagged|defaults doesn't work Review.
- #flagfilter global processor/allocator not working 2023-08
-
- set opts [check_flags {*}$sort_flagspecs -debugargs $debugargs -values $args]
-
- #we can only shortcircuit input list of single element at this point if there aren't non-default -inputformat or -outputformat transformations
- if {[llength $stringlist] == 1} {
- set is_basic 1
- foreach fname [list -inputformat -outputformat] {
- if {[dict get $sort_flagspecs -defaults $fname] ne [dict get $opts $fname]} {
- set is_basic 0
- break
- }
- }
- if {$is_basic} {
- return $stringlist
- }
- }
-
-
- set winlike [dict get $opts -winlike]
- set topchars [dict get $opts -topchars]
- set cols [dict get $opts -cols]
- set debug [dict get $opts -debug]
- set stacktrace [dict get $opts -stacktrace]
- set showsplits [dict get $opts -showsplits]
- set splits [dict get $opts -splits]
- set sortmethod [dict get $opts -sortmethod]
- set opt_collate [dict get $opts -collate]
- set opt_inputformat [dict get $opts -inputformat]
- set opt_inputformatapply [dict get $opts -inputformatapply]
- set opt_inputformatoptions [dict get $opts -inputformatoptions]
- set opt_outputformat [dict get $opts -outputformat]
- set opt_outputformatoptions [dict get $opts -outputformatoptions]
-
- if {$debug} {
- #dict unset opts -showsplits
- #dict unset opts -splits
- puts stdout "natsort::sort processed_args: $opts"
- if {$debug == 1} {
- puts stdout "natsort::sort - try also -debug 2, -debug 3"
- }
- }
-
- #set sortmethod "-dictionary" ;# sorts a2b before a001b - possibly other strangenesses that are hard to reason about
- switch -- $sortmethod {
- dictionary - ascii {
- set sortmethod "-$sortmethod"
- # -ascii is default for tcl lsort.
- }
- default {
- set sortmethod "-ascii"
- }
- }
-
- set allowed_collations [list nocase]
- if {$opt_collate ne "\uFFFF"} {
- if {$opt_collate ni $allowed_collations} {
- error "natsort::sort unknown value for -collate option. Only acceptable value(s): $allowed_collations"
- }
- set nocaseopt "-$opt_collate"
- } else {
- set nocaseopt ""
- }
- set allowed_inputformats [list tcl raw csv words]
- switch -- $opt_inputformat {
- tcl - raw - csv - words {}
- default {
- error "natsort::sort unknown value for -inputformat option. Only acceptable value(s): $allowed_inputformats"
- }
- }
- set allowed_outputformats [list tcl raw csv words]
- switch -- $opt_outputformat {
- tcl - raw - csv - words {}
- default {
- error "natsort::sort unknown value for -outputformat option. Only acceptable value(s): $allowed_outputformats"
- }
- }
-
- #
- set winsplits [list / . _]
- set commonsplits [list / . _ -]
- #set commonsplits [list]
-
- set tagconfig [dict create]
- dict set tagconfig last_part_text_tag "<19>"
- if {$winlike} {
- set splitchars $winsplits
- #windows explorer sorts leading spaces at the top - which doesn't seem very helpful for keeping things together - but the explorer doesn't seem able to create leading spaces anyway.
- set wintop [list "(" ")" { } {.} {_}] ;#windows specific order
- foreach t $topchars {
- if {$t ni $wintop} {
- lappend wintop $t
- }
- }
- set topchars $wintop
- dict set tagconfig last_part_text_tag ""
- } else {
- set splitchars $commonsplits
- }
- if {$splits ne "\uFFFF"} {
- set splitchars $splits
- }
- dict set tagconfig original_splitchars $splitchars
- dict set tagconfig showsplits $showsplits
-
- #create topdict
- set i 0
- set topdict [dict create]
- foreach c $topchars {
- incr i ;#start at 01 so that 00 reserved for final-split tag (allows x-0.1.txt to sort above x-0.1.1.txt by default. Use tagconfig to change, or choose -winlike 1 for explorer-like sorting)
- dict set topdict $c "<0$i>"
- }
- set keylist [list]
-
- switch -- $opt_inputformat {
- tcl {
- set lineinput_transform [list _lineinput_as_tcl $opt_inputformatoptions]
- }
- csv {
- set lineinput_transform [list _lineinput_as_csv $opt_inputformatoptions]
- }
- raw {
- set lineinput_transform [list _lineinput_as_raw $opt_inputformatoptions]
- }
- words {
- set lineinput_transform [list _lineinput_as_words $opt_inputformatoptions]
- }
- }
- switch -- $opt_outputformat {
- tcl {
- set lineoutput_transform [list _lineoutput_as_tcl $opt_outputformatoptions]
- }
- csv {
- set lineoutput_transform [list _lineoutput_as_csv $opt_outputformatoptions]
- }
- raw {
- set lineoutput_transform [list _lineoutput_as_raw $opt_outputformatoptions]
- }
- words {
- set lineoutput_transform [list _lineoutput_as_words $opt_outputformatoptions]
- }
- }
-
- if {("data" in $opt_inputformatapply) || ("index" in $opt_inputformatapply)} {
- if {$opt_inputformat eq "raw"} {
- set tf_stringlist $stringlist
- } else {
- set tf_stringlist [list]
- foreach v $stringlist {
- lappend tf_stringlist [{*}$lineinput_transform $v]
- }
- }
- if {"data" in $opt_inputformatapply} {
- set tf_data_stringlist $tf_stringlist
- } else {
- set tf_data_stringlist $stringlist
- }
- if {"index" in $opt_inputformatapply} {
- set tf_index_stringlist $tf_stringlist
- } else {
- set tf_index_stringlist $stringlist
- }
- } else {
- set tf_data_stringlist $stringlist
- set tf_index_stringlist $stringlist
- }
-
-
-
- if {$stacktrace} {
- puts stdout [natsort::stacktrace]
- set natsort::stacktrace_on 1
- }
- if {$cols eq "\uFFFF"} {
- set colkeys [lmap v $stringlist {}]
- } else {
- set colkeys [list]
- foreach v $tf_index_stringlist {
- set lineparts $v
- set k [list]
- foreach c $cols {
- lappend k [lindex $lineparts $c]
- }
- lappend colkeys [join $k "_"] ;#use a common-split char - Review
- }
- }
- #puts stdout "colkeys: $colkeys"
-
- if {$opt_inputformat eq "raw"} {
- #no inputformat was applied - can just use stringlist
- foreach value $stringlist ck $colkeys {
- set contentindex [build_key $value $splitchars $topdict $tagconfig $debug]
- set colindex [build_key $ck $splitchars $topdict $tagconfig $debug]
- lappend keylist ${colindex}-${contentindex}-$value ;#note: entire raw value used for final sort disambiguation (can be whitespace that was ignored in indexing)
- }
- } else {
- foreach keyinput $tf_index_stringlist datavalue $tf_data_stringlist ck $colkeys {
- #data may or may not have been transformed
- #column index may or may not have been built with transformed data
-
- set contentindex [build_key $keyinput $splitchars $topdict $tagconfig $debug]
- set colindex [build_key $ck $splitchars $topdict $tagconfig $debug]
- lappend keylist ${colindex}-${contentindex}-$datavalue ;#note: entire value used for final sort disambiguation (can be whitespace that was ignored in indexing)
- }
- }
- #puts stderr "keylist: $keylist"
-
- ###################################################################################################
- # Use the generated keylist to do the actual sorting
- # select either the transformed or raw data as the corresponding output
- ###################################################################################################
- if {[string length $nocaseopt]} {
- set sortcommand [list lsort $sortmethod $nocaseopt -indices $keylist]
- } else {
- set sortcommand [list lsort $sortmethod -indices $keylist]
- }
- if {$opt_outputformat eq "raw"} {
- #raw output means no further transformations - it doesn't mean there wasn't a transform applied on the input side
- #use the tf_data_stringlist in the output - which will be the same as the input stringlist if no input transform applied for data.
- #(Also - it may or may not have been *sorted* on transformed data depending on whether 'index' was in $opt_inputformatapply)
- foreach idx [{*}$sortcommand] {
- lappend result [lindex $tf_data_stringlist $idx]
- }
- } else {
- #we need to apply an output format
- #The data may or may not have been transformed at input
- foreach idx [{*}$sortcommand] {
- lappend result [{*}$lineoutput_transform [lindex $tf_data_stringlist $idx]]
- }
- }
- ###################################################################################################
-
-
-
-
-
- if {$debug >= 2} {
- set screen_width 250
- set max_val 0
- set max_idx 0
- ##### calculate colum widths
- foreach i [{*}$sortcommand] {
- set len_val [string length [lindex $stringlist $i]]
- if {$len_val > $max_val} {
- set max_val $len_val
- }
- set len_idx [string length [lindex $keylist $i]]
- if {$len_idx > $max_idx} {
- set max_idx $len_idx
- }
- }
- ####
- set l_width [expr {$max_val + 1}]
- set leftcol [string repeat " " $l_width]
- set r_width [expr {$screen_width - $l_width - 1}]
- set rightcol [string repeat " " $r_width]
- set str [overtype::left $leftcol RAW]
- puts stdout " $str Index with possibly transformed data at tail"
- foreach i [{*}$sortcommand] {
- #puts stdout "|d> [overtype::left $leftcol [lindex $stringlist $i] ] [lindex $keylist $i]"
- set index [lindex $keylist $i]
- set len_idx [string length $index]
- set rowcount [expr {$len_idx / $r_width}]
- if {($len_idx % $r_width) > 0} {
- incr rowcount
- }
- set rows [list]
- for {set r 0} {$r < $rowcount} {incr r} {
- lappend rows [string range $index 0 $r_width-$r]
- set index [string range $index $r_width end]
- }
-
- set r 0
- foreach idxpart $rows {
- if {$r == 0} {
- #use the untransformed stringlist
- set str [overtype::left $leftcol [lindex $stringlist $i]]
- } else {
- set str [overtype::left $leftcol ...]]
- }
- puts stdout " $str $idxpart"
- incr r
- }
- #puts stdout "|> '[lindex $stringlist $i]'"
- #puts stdout "|> [lindex $keylist $i]"
- }
-
- puts stdout "|debug> topdict: $topdict"
- puts stdout "|debug> splitchars: $splitchars"
- }
- return $result
- }
-
-
-
- #Note that although leading whitespace isn't a commonly used feature of filesystem names - it's possible at least on FreeBSD,windows and linux so we should try to handle it sensibly.
- proc sort_experiment {stringlist args} {
- package require sqlite3
-
- variable debug
- set args [check_flags -caller natsort::sort \
- -defaults [dict create -db :memory: -collate nocase -nullvalue "->NULL<" -winlike 0 -topchars [list] -debug 0] \
- -extras {all} \
- -values $args]
- set db [string trim [dict get $args -db]]
- set collate [string trim [dict get $args -collate]]
- set winlike [string trim [dict get $args -winlike]]
- set debug [string trim [dict get $args -debug]]
- set nullvalue [string trim [dict get $args -nullvalue]]
-
-
- set topchars [string trim [dict get $args -topchars]]
-
- set topdot [expr {"." in $topchars}]
- set topunderscore [expr {"_" in $topchars}]
-
-
- sqlite3 db_natsort2 $db
- #--
- #our table must handle the name with the greatest number of numeric/non-numeric splits.
- #This means a single list member with pathological naming e.g a1a1a1a1a1a1a1a1a1a1a1.txt could greatly extend the number of columns and indices and affect performance.
- #review: could be optimised to aggregate the tail into a single index, as the the extra columns won't assist in ordering, but make the table and query bigger.
- # we should probably determine the longest common sequence of splits in the input list and add only one more index for the segment after that.
- set maxsegments 0
- #--
- set prefix "idx"
-
- #note - there will be more columns in the sorting table than segments.
- # (a segment equals one of the numeric & non-numeric string portions returned from 'split_numeric_sgements')
- #---------------------------
- # consider
- # a123b.v1.2.txt
- # a123b.v1.3beta1.txt
- # these have the following segments:
- # a 123 b.v 1 . 2 .txt
- # a 123 b.v 1 . 3 beta 1 .txt
- #---------------------------
- # The first string has 7 segments (numbered 0 to 6)
- # the second string has 9 segments
- #
- # for example when the data has any elements in a segment position that are numeric (e.g 0001 123)
- # - then an index column with numeric equivalents will be created (e.g 0001 becomes 1), and any non-numeric values in that column will get mapped to a negative value (for special cases) or a high value such as NULL (with NULLS LAST sql support)
- #
- # when a segment
-
- #cycle through all strings - we cannot build tabledef as we go because the column type depends on whether all segments for a particular column are text vs int-equivalent.
- array set segmentinfo {}
- foreach nm $stringlist {
- set segments [split_numeric_segments $nm]
- if {![string length [string trim [lindex $segments 0]]]} {
- if {[string is digit [string trim [lindex $segments 1]]]} {
- #name is whitespace followed by a digit - special case - ignore the whitespace for numbers only. (whitespace still goes through to name column though)
- set segments [lrange $segments 1 end]
- }
- }
-
-
- set c 0 ;#start of index columns
- if {[llength $segments] > $maxsegments} {
- set maxsegments [llength $segments]
- }
- foreach seg $segments {
- set seg [string trim $seg]
- set column_exists [info exists segmentinfo($c,type)]
- if {[string is digit $seg]} {
- if {$column_exists} {
- #override it (may currently be text or int)
- set segmentinfo($c,type) "int"
- } else {
- #new column
- set segmentinfo($c,name) ${prefix}$c
- set segmentinfo($c,type) "int"
- }
- } else {
- #text never overrides int
- if {!$column_exists} {
- set segmentinfo($c,name) ${prefix}$c
- set segmentinfo($c,type) "text"
- }
- }
- incr c
- }
- }
- if {$debug} {
- puts stdout "Largest number of num/non-num segments in data: $maxsegments"
- #parray segmentinfo
- }
-
- #
- set tabledef ""
- set ordered_column_names [list]
- set ordered_segmentinfo_tags [lsort -dictionary [array names segmentinfo *]]
- foreach k $ordered_segmentinfo_tags {
- lassign [split $k ,] c tag
- if {$tag eq "type"} {
- set type [set segmentinfo($k)]
- if {$type eq "int"} {
- append tabledef "$segmentinfo($c,name) int,"
- } else {
- append tabledef "$segmentinfo($c,name) text COLLATE $collate,"
- }
- append tabledef "raw$c text COLLATE $collate,"
- lappend ordered_column_names $segmentinfo($c,name)
- lappend ordered_column_names raw$c ;#additional index column not in segmentinfo
- }
- if {$tag eq "name"} {
- #lappend ordered_column_names $segmentinfo($k)
- }
- }
- append tabledef "name text"
-
- #puts stdout "tabledef:$tabledef"
-
-
- db_natsort2 eval [string map [list %tabledef% $tabledef] {create table natsort(%tabledef%)}]
-
-
- foreach nm $stringlist {
- array unset intdata
- array set intdata {}
- array set rawdata {}
- #init array and build sql values string
- set sql_insert "insert into natsort values("
- for {set i 0} {$i < $maxsegments} {incr i} {
- set intdata($i) ""
- set rawdata($i) ""
- append sql_insert "\$intdata($i),\$rawdata($i),"
- }
- append sql_insert "\$nm" ;#don't manipulate name value in any way - e.g must leave all whitespace as the sort must return exactly the same elements as in the original list.
- append sql_insert ")"
-
- set segments [split_numeric_segments $nm]
- if {![string length [string trim [lindex $segments 0]]]} {
- if {[string is digit [string trim [lindex $segments 1]]]} {
- #name is whitespace followed by a digit - special case - ignore the whitespace for numbers only. (whitespace still goes through to name column though)
- set segments [lrange $segments 1 end]
- }
- }
- set values ""
- set c 0
- foreach seg $segments {
- if {[set segmentinfo($c,type)] eq "int"} {
- if {[string is digit [string trim $seg]]} {
- set intdata($c) [trimzero [string trim $seg]]
- } else {
- catch {unset intdata($c)} ;#set NULL - sorts last
- if {($c == 0) && ($topunderscore) && [string match _* [string trim $seg]]} {
- set intdata($c) -100
- }
- if {($c == 0) && ($topdot) && [string match .* [string trim $seg]]} {
- set intdata($c) -50
- }
- }
- set rawdata($c) [string trim $seg]
- } else {
- #pure text column
- #set intdata($c) [string trim $seg] ;#ignore leading/trailing whitespace - we sort first on trimmed version, then refine with the sort on rawdata index
- #catch {unset indata($c)}
- set indata($c) [string trim $seg]
- set rawdata($c) $seg
- }
- #set rawdata($c) [string trim $seg]#
- #set rawdata($c) $seg
- incr c
- }
- db_natsort2 eval $sql_insert
- }
-
- set orderedlist [list]
-
- if {$debug} {
- db_natsort2 eval {select * from pragma_table_info('natsort')} rowdata {
- parray rowdata
- }
- }
- set orderby "order by "
-
- foreach cname $ordered_column_names {
- if {[string match "idx*" $cname]} {
- append orderby "$cname ASC NULLS LAST,"
- } else {
- append orderby "$cname ASC,"
- }
- }
- append orderby " name ASC"
- #append orderby " NULLS LAST" ;#??
-
- #e.g "order by idx0 ASC, raw0 ASC, idx1 ASC .... name ASC"
- if {$debug} {
- puts stdout "orderby clause: $orderby"
- }
- db_natsort2 eval [string map [list %orderby% $orderby] {select * from natsort %orderby%}] rowdata {
- set line "- "
- #parray rowdata
- set columnnames $rowdata(*)
- #puts stdout "columnnames: $columnnames"
- #[lsort -dictionary [array names rowdata]
- append line "$rowdata(name) \n"
- foreach nm $columnnames {
- if {$nm ne "name"} {
- append line "$nm: $rowdata($nm) "
- }
- }
- #puts stdout $line
- #puts stdout "$rowdata(name)"
- lappend orderedlist $rowdata(name)
- }
-
- db_natsort2 close
- return $orderedlist
- }
-}
-
-
-#application section e.g this file might be linked from /usr/local/bin/natsort
-namespace eval natsort {
- namespace import ::flagfilter::check_flags
-
- proc called_directly_namematch {} {
- global argv0
- if {[info script] eq ""} {
- return 0
- }
- #see https://wiki.tcl-lang.org/page/main+script
- #trailing ... let's us resolve symlinks in last component of the path (could be something else like ___ but ... seems unlikely to collide with anything in the filesystem)
- if {[info exists argv0]
- &&
- [file dirname [file normalize [file join [info script] ...]]]
- eq
- [file dirname [file normalize [file join $argv0 ...]]]
- } {
- return 1
- } else {
- #puts stdout "norm info script: [file dirname [file normalize [file join [info script] ...]]]"
- #puts stdout "norm argv0 : [file dirname [file normalize [file join $argv0 ...]]]"
- return 0
- }
- }
- #Review issues around comparing names vs using inodes (esp with respect to samba shares)
- proc called_directly_inodematch {} {
- global argv0
-
- if {[info exists argv0]
- && [file exists [info script]] && [file exists $argv0]} {
- file stat $argv0 argv0Info
- file stat [info script] scriptInfo
- if {$argv0Info(ino) == 0 || $scriptInfo(ino) == 0 || $argv0Info(dev) == 0 || $scriptInfo(dev) == 0} {
- #vfs?
- #e.g //zipfs:/
- return 0
- }
- return [expr {$argv0Info(dev) == $scriptInfo(dev)
- && $argv0Info(ino) == $scriptInfo(ino)}]
- } else {
- return 0
- }
- }
-
- if {![interp issafe]} {
- set is_namematch [called_directly_namematch]
- set is_inodematch [called_directly_inodematch]
- ####
- #review - reliability of mechanisms to determine direct calls
- # we don't want application being called when being used as a library, but we need it to run if called directly or from symlinks etc
- #-- choose a policy and leave the others commented.
- #set is_called_directly $is_namematch
- #set is_called_directly $is_inodematch
-
- #puts "NATSORT: called_directly_namematch - $is_namematch"
- #puts "NATSORT: called_directly_inodematch - $is_inodematch"
- #flush stdout
-
- set is_called_directly [expr {$is_namematch || $is_inodematch}]
- #set is_called_directly [expr {$is_namematch && $is_inodematch}]
- ###
-
-
- #puts stdout "called_directly_name: [called_directly_namematch] called_directly_inode: [called_directly_inodematch]"
- } else {
- #safe interp
- set is_called_directly 0
- }
-
-
-
- proc test_pass_fail_message {pass {additional ""}} {
- variable test_fail_msg
- variable test_pass_msg
- if {$pass} {
- puts stderr $test_pass_msg
- } else {
- puts stderr $test_fail_msg
- }
- puts stderr $additional
- }
-
- variable test_fail_msg "XXXXXXXXXXXX FAIL XXXXXXXXXXXXX"
- variable test_pass_msg "------------ PASS -------------"
- proc test_sort_1 {args} {
- package require struct::list
- puts stderr "---$args"
- set args [check_flags -caller natsort:test_sort_1 -defaults [list -collate nocase -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0 ] -values $args]
-
- puts stderr "test_sort_1 got args: $args"
-
- set unsorted_input {
- 2.2.2
- 2.2.2.2
- 1a.1.1
- 1a.2.1.1
- 1.12.1
- 1.2.1.1
- 1.02.1.1
- 1.002b.1.1
- 1.1.1.2
- 1.1.1.1
- }
- set input {
-1.1.1
-1.1.1.2
-1.002b.1.1
-1.02.1.1
-1.2.1.1
-1.12.1
-1a.1.1
-1a.2.1.1
-2.2.2
-2.2.2.2
- }
-
- set sorted [natsort::sort $input {*}$args]
- set is_match [struct::list equal $input $sorted]
-
- set msg "windows-explorer order"
-
- test_pass_fail_message $is_match $msg
- puts stdout [string repeat - 40]
- puts stdout INPUT
- puts stdout [string repeat - 40]
- foreach item $input {
- puts stdout $item
- }
- puts stdout [string repeat - 40]
- puts stdout OUTPUT
- puts stdout [string repeat - 40]
- foreach item $sorted {
- puts stdout $item
- }
- test_pass_fail_message $is_match $msg
- return [expr {!$is_match}]
- }
- proc test_sort_showsplits {args} {
- package require struct::list
-
- set args [check_flags -caller natsort:test_sort_1 \
- -defaults [list -collate nocase -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 1 ] \
- -extras {all} \
- -values $args]
-
- set input1 {
- a-b.txt
- a.b.c.txt
- b.c-txt
- }
-
-
- set input2 {
- a.b.c.txt
- a-b.txt
- b.c-text
- }
-
- foreach {msg testlist } [list "custom-order" $input1 "windows-explorer (should work with -winlike 1)" $input2] {
- set sorted [natsort::sort $testlist {*}$args]
- set is_match [struct::list equal $testlist $sorted]
-
- test_pass_fail_message $is_match $msg
- puts stderr "INPUT"
- puts stderr "[string repeat - 40]"
- foreach item $testlist {
- puts stdout $item
- }
- puts stderr "[string repeat - 40]"
- puts stderr "OUTPUT"
- puts stderr "[string repeat - 40]"
- foreach item $sorted {
- puts stdout $item
- }
-
- test_pass_fail_message $is_match $msg
- }
-
- #return [expr {!$is_match}]
-
- }
-
- #tcl proc dispatch order - non flag items up front
- #trailing flags are paired even if supplied as solo flags e.g -l becomes -l 1
- proc commandline_ls {args} {
- set operands [list]
- set posn 0
- foreach a $args {
- if {![string match -* $a]} {
- lappend operands $a
- } else {
- set flag1_posn $posn
- break
- }
- incr posn
- }
- set args [lrange $args $flag1_posn end]
-
-
- set debug 0
- set posn [lsearch $args -debug]
- if {$posn > 0} {
- if {[lindex $args $posn+1]} {
- set debug [lindex $args $posn+1]
- }
- }
- if {$debug} {
- puts stderr "|debug>commandline_ls got $args"
- }
-
- #if first operand not supplied - replace it with current working dir
- if {[lindex $operands 0] eq "\uFFFF"} {
- lset operands 0 [pwd]
- }
-
- set targets [list]
- foreach op $operands {
- if {$op ne "\uFFFF"} {
- set opchars [split [file tail $op] ""]
- if {"?" in $opchars || "*" in $opchars} {
- lappend targets $op
- } else {
- #actual file or dir
- set targetitem $op
- set targetitem [file normalize $op]
- if {![file exists $targetitem]} {
- if {$debug} {
- puts stderr "|debug>commandline_ls Unable to access path '$targetitem'"
- }
- }
- lappend targets $targetitem
- if {$debug} {
- puts stderr "|debug>commandline_ls listing for $targetitem"
- }
- }
- }
- }
- set args [check_flags -caller commandline_ls \
- -return flagged|defaults \
- -debugargs 0 \
- -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0 -algorithm sort] \
- -required {all} \
- -extras {all} \
- -soloflags {-v -l} \
- -commandprocessors {} \
- -values $args ]
- if {$debug} {
- puts stderr "|debug>args: $args"
- }
-
-
- set algorithm [dict get $args -algorithm]
- dict unset args -algorithm
-
- set allfolders [list]
- set allfiles [list]
- foreach item $targets {
- if {[file exists $item]} {
- if {[file type $item] eq "directory"} {
- set dotfolders [glob -nocomplain -directory $item -type {d} -tail .*]
- set folders [glob -nocomplain -directory $item -type {d} -tail *]
- set allfolders [concat $allfolders $dotfolders $folders]
-
- set dotfiles [glob -nocomplain -directory $item -type {f} -tail .*]
- set files [glob -nocomplain -directory $item -type {f} -tail *]
- set allfiles [concat $allfiles $dotfiles $files]
- } else {
- #file (or link?)
- set files [glob -nocomplain -directory [file dirname $item] -tail [file tail $item]]
- set allfiles [concat $allfiles $files]
- }
- } else {
- set folders [glob -nocomplain -directory $item -type {d} -tail [file tail $item]]
- set allfolders [concat $allfolders $folders]
- set files [glob -nocomplain -directory [file dirname $item] -tail [file tail $item]]
- set allfiles [concat $allfiles $files]
- }
- }
-
-
- set sorted_folders [natsort::sort $allfolders {*}$args]
- set sorted_files [natsort::sort $allfiles {*}$args]
-
- foreach fold $sorted_folders {
- puts stdout $fold
- }
- foreach file $sorted_files {
- puts stdout $file
- }
-
- return "-- ok printed to stdout [llength $sorted_folders] folders and [llength $sorted_files] files --"
- }
-
- package require argp
- argp::registerArgs commandline_test {
- { -showsplits boolean 0}
- { -stacktrace boolean 0}
- { -debug boolean 0}
- { -winlike boolean 0}
- { -db string ":memory:"}
- { -collate string "nocase"}
- { -algorithm string "sort"}
- { -topchars string "\uFFFF"}
- { -testlist string {10 1 30 3}}
- }
- argp::setArgsNeeded commandline_test {-stacktrace}
- proc commandline_test {test args} {
- variable testlist
- puts stdout "commandline_test got $args"
- argp::parseArgs opts
- puts stdout "commandline_test got [array get opts]"
- set args [check_flags -caller natsort_commandline \
- -return flagged|defaults \
- -defaults [list -db :memory: -collate nocase -testlist $testlist -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] \
- -values $args]
-
- if {[string tolower $test] in [list "1" "true"]} {
- set test "sort"
- } else {
- if {![llength [info commands $test]]} {
- error "test $test not found"
- }
- }
- dict unset args -test
- set stacktrace [dict get $args -stacktrace]
- # dict unset args -stacktrace
-
- set argtestlist [dict get $args -testlist]
- dict unset args -testlist
-
-
- set debug [dict get $args -debug]
-
- set collate [dict get $args -collate]
- set db [dict get $args -db]
- set winlike [dict get $args -winlike]
- set topchars [dict get $args -topchars]
-
-
- puts stderr "|test>-----start natsort::$test--- input list size : [llength $argtestlist]"
- #set resultlist [$test $argtestlist -db $db -collate $collate -topchars $topchars -winlike $winlike]
- set resultlist [$test $argtestlist {*}$args]
- foreach nm $resultlist {
- puts stdout $nm
- }
- puts stdout "|test>-----end natsort::$test--- sorted list size: [llength $resultlist]"
- return "test end"
- }
- proc commandline_runtests {runtests args} {
- set argvals [check_flags -caller commandline_runtests \
- -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits "\uFFFF" -runtests 1] \
- -values $args]
-
- puts stderr "runtests args: $argvals"
-
- #set runtests [dict get $argvals -runtests]
- dict unset argvals -runtests
- dict unset argvals -algorithm
-
- puts stderr "runtests args: $argvals"
- #exit 0
-
- set test_prefix "::natsort::test_sort_"
-
- if {$runtests eq "1"} {
- set runtests "*"
- }
-
-
- set testcommands [info commands ${test_prefix}${runtests}]
- if {![llength $testcommands]} {
- puts stderr "No test commands matched -runtests argument '$runtests'"
- puts stderr "Use 1 to run all tests"
- set alltests [info commands ${test_prefix}*]
- puts stderr "Valid tests are:"
-
- set prefixlen [string length $test_prefix]
- foreach t $alltests {
- set shortname [string range $t $prefixlen end]
- puts stderr "$t = -runtests $shortname"
- }
-
- } else {
- foreach cmd $testcommands {
- puts stderr [string repeat - 40]
- puts stderr "calling $cmd with args: '$argvals'"
- puts stderr [string repeat - 40]
- $cmd {*}$argvals
- }
- }
- exit 0
- }
- proc help {args} {
- puts stdout "natsort::help got '$args'"
- return "Help not implemented"
- }
- proc natsort_pipe {args} {
- #PIPELINE to take input list on stdin and write sorted list to stdout
- #strip - from arglist
- #set args [check_flags -caller natsort_pipeline \
- # -return all \
- # -defaults [list -db :memory: -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] \
- # -values $args]
-
-
- set debug [dict get $args -debug]
- if {$debug} {
- puts stderr "|debug> natsort_pipe got args:'$args'"
- }
- set algorithm [dict get $args -algorithm]
- dict unset args -algorithm
-
- set proclist [info commands ::natsort::sort*]
- set algos [list]
- foreach p $proclist {
- lappend algos [namespace tail $p]
- }
- if {$algorithm ni [list {*}$proclist {*}$algos]} {
- do_error "valid sort mechanisms: $algos" 2
- }
-
-
- set input_list [list]
- while {![eof stdin]} {
- if {[gets stdin line] > 0} {
- lappend input_list $line
- } else {
- if {[eof stdin]} {
-
- } else {
- after 10
- }
- }
- }
-
- if {$debug} {
- puts stderr "|debug> received [llength $input_list] list elements"
- }
-
- set resultlist [$algorithm $input_list {*}$args]
- if {$debug} {
- puts stderr "|debug> returning [llength $resultlist] list elements"
- }
- foreach r $resultlist {
- puts stdout $r
- }
- #exit 0
-
- }
- if {($is_called_directly)} {
- set cmdprocessors {
- {helpfinal {match "^help$" dispatch natsort::help}}
- {helpfinal {sub -topic default "NONE"}}
- }
- #set args [check_flags \
- # -caller test1 \
- # -debugargs 2 \
- # -return arglist \
- # -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] \
- # -required {none} \
- # -extras {all} \
- # -commandprocessors $cmdprocessors \
- # -values $::argv ]
- interp alias {} do_filter {} ::flagfilter::check_flags
-
- #mashopts are generally single-letter opts that can be run together e.g -l -d as -ld
- set cmdprocessors {
- {helpcmd {match "^help$" dispatch natsort::help singleopts {-v}}}
- {helpcmd {sub -operand default \uFFFF singleopts {-l}}}
- {lscmd {match "^ls$" dispatch natsort::commandline_ls dispatchtype tcl dispatchglobal 1 mashopts {-l -a} singleopts {-l -a} pairopts {} longopts {--color=always}}}
- {lscmd {sub dir default "\uFFFF"}}
- {lscmd {sub dir2 default "\uFFFF"}}
- {lscmd {sub dir3 default "\uFFFF"}}
- {lscmd {sub dir4 default "\uFFFF"}}
- {lscmd {sub dir5 default "\uFFFF"}}
- {lscmd {sub dir6 default "\uFFFF"}}
- {runtests {match "^-tests$" dispatch natsort::commandline_runtests singleopts {-l}}}
- {runtests {sub testname default "1" singleopts {-l}}}
- {pipecmd {match "^-$" dispatch natsort::natsort_pipe dispatchtype tcl}}
- }
- set arglist [do_filter \
- -debugargs 0 \
- -debugargsonerror 2 \
- -caller cline_dispatch1 \
- -return all \
- -soloflags {-v -x} \
- -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0 ] \
- -required {all} \
- -extras {all} \
- -commandprocessors $cmdprocessors \
- -values $::argv ]
-
-
- #mashopts are generally single-letter opts that can be run together e.g -l -d as -ld
- set cmdprocessors {
- {testcmd {match "^test$" dispatch natsort::commandline_test singleopts {-l}}}
- {testcmd {sub testname default "1" singleopts {-l}}}
- }
- set arglist [check_flags \
- -debugargs 0 \
- -caller cline_dispatch2 \
- -return all \
- -soloflags {-v -l} \
- -defaults [list -collate nocase -algorithm sort -testlist "1 2 3 10" -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0 ] \
- -required {all} \
- -extras {all} \
- -commandprocessors $cmdprocessors \
- -values $::argv ]
-
-
-
-
- #set cmdprocessors [list]
- #set args [check_flags -caller test1 -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] -required {all} -extras {none} -commandprocessors $cmdprocessors -values $::argv ]
-
- #set args [check_flags -caller test1 -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] -required {all} -extras {none} -commandprocessors {-cmd {-cmd -cmdarg1 -default "."} {-cmd -cmdarg2 -default j}} -values $::argv ]
- #set args [check_flags -caller test1 -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] -required {all} -extras {none} -commandprocessors {{-cmd -default help} {-cmd -cmdarg1 -default "."} {-cmd -cmdarg2 -default j}} -values $::argv ]
- #set args [check_flags -caller test1 -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] -required {all} -extras {none} -commandprocessors {ls {ls lsdir -default "\uFFFF"}} -values $::argv ]
-
- puts stderr "natsort directcall exit"
- flush stderr
- exit 0
-
- if {$::argc} {
-
- }
- }
-}
-
-
-package provide natsort [namespace eval natsort {
- variable version
- set version 0.1.1.6
-}]
-
-
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/oolib-0.1.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/oolib-0.1.2.tm
deleted file mode 100644
index 858c61cd..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/oolib-0.1.2.tm
+++ /dev/null
@@ -1,201 +0,0 @@
-#JMN - api should be kept in sync with package patternlib where possible
-#
-package provide oolib [namespace eval oolib {
- variable version
- set version 0.1.2
-}]
-
-namespace eval oolib {
- oo::class create collection {
- variable o_data ;#dict
- #variable o_alias
- constructor {} {
- set o_data [dict create]
- }
- method info {} {
- return [dict info $o_data]
- }
- method count {} {
- return [dict size $o_data]
- }
- method isEmpty {} {
- expr {[dict size $o_data] == 0}
- }
- method names {{globOrIdx {}}} {
- if {[llength $globOrIdx]} {
- if {[string is integer -strict $globOrIdx]} {
- set idx $globOrIdx
- if {$idx < 0} {
- set idx "end-[expr {abs($idx + 1)}]"
- }
- if {[catch {lindex [dict keys $o_data] $idx} result]} {
- error "[self object] no such index : '$idx'"
- } else {
- return $result
- }
- } else {
- #glob
- return [lsearch -glob -all -inline [dict keys $o_data] $globOrIdx]
- }
- } else {
- return [dict keys $o_data]
- }
- }
- #like names but without globbing
- method keys {} {
- dict keys $o_data
- }
- method key {{posn 0}} {
- if {$posn < 0} {
- set posn "end-[expr {abs($posn + 1)}]"
- }
- if {[catch {lindex [dict keys $o_data] $posn} result]} {
- error "[self object] no such index : '$posn'"
- } else {
- return $result
- }
- }
- method hasKey {key} {
- dict exists $o_data $key
- }
- method get {} {
- return $o_data
- }
- method items {} {
- return [dict values $o_data]
- }
- method item {key} {
- if {[string is integer -strict $key]} {
- if {$key >= 0} {
- set valposn [expr {(2*$key) +1}]
- return [lindex $o_data $valposn]
- } else {
- set key "end-[expr {abs($key + 1)}]"
- return [lindex $o_data $key]
- #return [lindex [dict keys $o_data] $key]
- }
- }
- if {[dict exists $o_data $key]} {
- return [dict get $o_data $key]
- }
- }
- #inverse lookup
- method itemKeys {value} {
- set value_indices [lsearch -all [dict values $o_data] $value]
- set keylist [list]
- foreach i $value_indices {
- set idx [expr {(($i + 1) *2) -2}]
- lappend keylist [lindex $o_data $idx]
- }
- return $keylist
- }
- method search {value args} {
- set matches [lsearch {*}$args [dict values $o_data] $value]
- if {"-inline" in $args} {
- return $matches
- } else {
- set keylist [list]
- foreach i $matches {
- set idx [expr {(($i + 1) *2) -2}]
- lappend keylist [lindex $o_data $idx]
- }
- return $keylist
- }
- }
- #review - see patternlib. Is the intention for aliases to be configurable independent of whether the target exists?
- #review - what is the point of alias anyway? - why slow down other operations when a variable can hold a keyname perfectly well?
- #method alias {newAlias existingKeyOrAlias} {
- # if {[string is integer -strict $newAlias]} {
- # error "[self object] collection key alias cannot be integer"
- # }
- # if {[string length $existingKeyOrAlias]} {
- # set o_alias($newAlias) $existingKeyOrAlias
- # } else {
- # unset o_alias($newAlias)
- # }
- #}
- #method aliases {{key ""}} {
- # if {[string length $key]} {
- # set result [list]
- # foreach {n v} [array get o_alias] {
- # if {$v eq $key} {
- # lappend result $n $v
- # }
- # }
- # return $result
- # } else {
- # return [array get o_alias]
- # }
- #}
- ##if the supplied index is an alias, return the underlying key; else return the index supplied.
- #method realKey {idx} {
- # if {[catch {set o_alias($idx)} key]} {
- # return $idx
- # } else {
- # return $key
- # }
- #}
- method add {value key} {
- if {[string is integer -strict $key]} {
- error "[self object] collection key must not be an integer. Use another structure if integer keys required"
- }
- if {[dict exists $o_data $key]} {
- error "[self object] col_processors object error: key '$key' already exists in collection"
- }
- dict set o_data $key $value
- return [expr {[dict size $o_data] - 1}] ;#return index of item
- }
- method remove {idx {endRange ""}} {
- if {[string length $endRange]} {
- error "[self object] collection error: ranged removal not yet implemented.. remove one item at a time"
- }
- if {[string is integer -strict $idx]} {
- if {$idx < 0} {
- set idx "end-[expr {abs($idx+1)}]"
- }
- set key [lindex [dict keys $o_data] $idx]
- set posn $idx
- } else {
- set key $idx
- set posn [lsearch -exact [dict keys $o_data] $key]
- if {$posn < 0} {
- error "[self object] no such index: '$idx' in this collection"
- }
- }
- dict unset o_data $key
- return
- }
- method clear {} {
- set o_data [dict create]
- return
- }
- method reverse_the_collection {} {
- #named slightly obtusely because reversing the data when there may be references held is a potential source of bugs
- #the name reverse_the_collection should make it clear that the object is being modified in place as opposed to simply 'reverse' which may imply a view/copy.
- #todo - consider implementing a get_reverse which provides an interface to the same collection without affecting original references, yet both allowing delete/edit operations.
- set dictnew [dict create]
- foreach k [lreverse [dict keys $o_data]] {
- dict set dictnew $k [dict get $o_data $k]
- }
- set o_data $dictnew
- return
- }
- #review - cmd as list vs cmd as script?
- method map {cmd} {
- set seed [list]
- dict for {k v} $o_data {
- lappend seed [uplevel #0 [list {*}$cmd $v]]
- }
- return $seed
- }
- method objectmap {cmd} {
- set seed [list]
- dict for {k v} $o_data {
- lappend seed [uplevel #0 [list $v {*}$cmd]]
- }
- return $seed
- }
- }
-
-}
-
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/overtype-1.6.6.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/overtype-1.6.6.tm
deleted file mode 100644
index b4e59ec6..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/overtype-1.6.6.tm
+++ /dev/null
@@ -1,4774 +0,0 @@
-# -*- tcl -*-
-# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt
-#
-# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
-# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# (C) Julian Noble 2003-2023
-#
-# @@ Meta Begin
-# Application overtype 1.6.6
-# Meta platform tcl
-# Meta license BSD
-# @@ Meta End
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-# doctools header
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-#*** !doctools
-#[manpage_begin overtype_module_overtype 0 1.6.6]
-#[copyright "2024"]
-#[titledesc {overtype text layout - ansi aware}] [comment {-- Name section and table of contents description --}]
-#[moddesc {overtype text layout}] [comment {-- Description at end of page heading --}]
-#[require overtype]
-#[keywords module text ansi]
-#[description]
-#[para] -
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-#*** !doctools
-#[section Overview]
-#[para] overview of overtype
-#[subsection Concepts]
-#[para] -
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Requirements
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-
-#*** !doctools
-#[subsection dependencies]
-#[para] packages used by overtype
-#[list_begin itemized]
-
-package require Tcl 8.6-
-package require textutil
-package require punk::lib ;#required for lines_as_list
-package require punk::ansi ;#required to detect, split, strip and calculate lengths
-package require punk::char ;#box drawing - and also unicode character width determination for proper layout of text with double-column-width chars
-package require punk::assertion
-#*** !doctools
-#[item] [package {Tcl 8.6}]
-#[item] [package textutil]
-#[item] [package punk::ansi]
-#[para] - required to detect, split, strip and calculate lengths of text possibly containing ansi codes
-#[item] [package punk::char]
-#[para] - box drawing - and also unicode character width determination for proper layout of text with double-column-width chars
-
-# #package require frobz
-# #*** !doctools
-# #[item] [package {frobz}]
-
-#*** !doctools
-#[list_end]
-
-#PERFORMANCE notes
-#overtype is very performance sensitive - used in ansi output all over the place ie needs to be optimised
-#NOTE use of tcl::dict::for tcl::string::range etc instead of ensemble versions. This is for the many tcl 8.6/8.7 interps which don't compile ensemble commands when in safe interps
-#similar for tcl::namespace::eval - but this is at least on some versions of Tcl - faster even in a normal interp. Review to see if that holds for Tcl 9.
-#for string map: when there are exactly 2 elements - it is faster to use a literal which has a special case optimisation in the c code
-#ie use tcl::string::map {\n ""} ... instead of tcl::string::map [list \n ""] ...
-#note that we can use unicode (e.g \uFF31) and other escapes such as \t within these curly braces - we don't have to use double quotes
-#generally using 'list' is preferred for the map as less error prone.
-#can also use: tcl::string::map "token $var" .. but be careful regarding quoting and whitespace in var. This should be used sparingly if at all.
-
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-#*** !doctools
-#[section API]
-
-
-#Julian Noble - 2003
-#Released under standard 'BSD license' conditions.
-#
-#todo - ellipsis truncation indicator for center,right
-
-#v1.4 2023-07 - naive ansi color handling - todo - fix tcl::string::range
-# - need to extract and replace ansi codes?
-
-tcl::namespace::eval overtype {
- namespace import ::punk::assertion::assert
- punk::assertion::active true
-
- namespace path ::punk::lib
-
- namespace export *
- variable default_ellipsis_horizontal "..." ;#fallback
- variable default_ellipsis_vertical "..."
- tcl::namespace::eval priv {
- proc _init {} {
- upvar ::overtype::default_ellipsis_horizontal e_h
- upvar ::overtype::default_ellipsis_vertical e_v
- set e_h [format %c 0x2026] ;#Unicode Horizontal Ellipsis
- set e_v [format %c 0x22EE]
- #The unicode ellipsis looks more natural than triple-dash which is centred vertically whereas ellipsis is at floorline of text
- #Also - unicode ellipsis has semantic meaning that other processors can interpret
- #unicode does also provide a midline horizontal ellipsis 0x22EF
-
- #set e [format %c 0x2504] ;#punk::char::charshort boxd_ltdshhz - Box Drawings Light Triple Dash Horizontal
- #if {![catch {package require punk::char}]} {
- # set e [punk::char::charshort boxd_ltdshhz]
- #}
- }
- }
- priv::_init
-}
-proc overtype::about {} {
- return "Simple text formatting. Author JMN. BSD-License"
-}
-
-tcl::namespace::eval overtype {
- variable grapheme_widths [tcl::dict::create]
-
- variable escape_terminals
- #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~).
- tcl::dict::set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"]
- #tcl::dict::set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic
- tcl::dict::set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals
-
- #self-contained 2 byte ansi escape sequences - review more?
- variable ansi_2byte_codes_dict
- set ansi_2byte_codes_dict [tcl::dict::create\
- "reset_terminal" "\u001bc"\
- "save_cursor_posn" "\u001b7"\
- "restore_cursor_posn" "\u001b8"\
- "cursor_up_one" "\u001bM"\
- "NEL - Next Line" "\u001bE"\
- "IND - Down one line" "\u001bD"\
- "HTS - Set Tab Stop" "\u001bH"\
- ]
-
- #debatable whether strip should reveal the somethinghidden - some terminals don't hide it anyway.
- # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\
-}
-
-
-
-
-proc overtype::string_columns {text} {
- if {[punk::ansi::ta::detect $text]} {
- #error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::ansistrip. Alternatively try punk::ansi::printing_length"
- set text [punk::ansi::ansistrip $text]
- }
- return [punk::char::ansifreestring_width $text]
-}
-
-#todo - consider a way to merge overtype::left/centre/right
-#These have similar algorithms/requirements - and should be refactored to be argument-wrappers over a function called something like overtype::renderblock
-#overtype::renderblock could render the input to a defined (possibly overflowing in x or y) rectangle overlapping the underlay.
-#(i.e not even necessariy having it's top left within the underlay)
-tcl::namespace::eval overtype::priv {
-}
-
-#could return larger than renderwidth
-proc _get_row_append_column {row} {
- #obsolete?
- upvar outputlines outputlines
- set idx [expr {$row -1}]
- if {$row <= 1 || $row > [llength $outputlines]} {
- return 1
- } else {
- upvar opt_expand_right expand_right
- upvar renderwidth renderwidth
- set existinglen [punk::ansi::printing_length [lindex $outputlines $idx]]
- set endpos [expr {$existinglen +1}]
- if {$expand_right} {
- return $endpos
- } else {
- if {$endpos > $renderwidth} {
- return $renderwidth + 1
- } else {
- return $endpos
- }
- }
- }
-}
-
-tcl::namespace::eval overtype {
- #*** !doctools
- #[subsection {Namespace overtype}]
- #[para] Core API functions for overtype
- #[list_begin definitions]
-
-
-
- #tcl::string::range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r
- #render onto an already-rendered (ansi already processed) 'underlay' string, a possibly ansi-laden 'overlay' string.
- #The underlay and overlay can be multiline blocks of text of varying line lengths.
- #The overlay may just be an ansi-colourised block - or may contain ansi cursor movements and cursor save/restore calls - in which case the apparent length and width of the overlay can't be determined as if it was a block of text.
- #This is a single-shot rendering of strings - ie there is no way to chain another call containing a cursor-restore to previously rendered output and have it know about any cursor-saves in the first call.
- # a cursor start position other than top-left is a possible addition to consider.
- #see editbuf in punk::repl for a more stateful ansi-processor. Both systems use loops over overtype::renderline
- proc renderspace {args} {
- #*** !doctools
- #[call [fun overtype::renderspace] [arg args] ]
- #[para] usage: ?-transparent [lb]0|1[rb]? ?-expand_right [lb]1|0[rb]? ?-ellipsis [lb]1|0[rb]? ?-ellipsistext ...? undertext overtext
-
- # @c overtype starting at left (overstrike)
- # @c can/should we use something like this?: 'format "%-*s" $len $overtext
- variable default_ellipsis_horizontal
-
- if {[llength $args] < 2} {
- error {usage: ?-width ? ?-startcolumn ? ?-transparent [0|1|]? ?-expand_right [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext}
- }
- set optargs [lrange $args 0 end-2]
- if {[llength $optargs] % 2 == 0} {
- set overblock [lindex $args end]
- set underblock [lindex $args end-1]
- #lassign [lrange $args end-1 end] underblock overblock
- set argsflags [lrange $args 0 end-2]
- } else {
- set optargs [lrange $args 0 end-1]
- if {[llength $optargs] %2 == 0} {
- set overblock [lindex $args end]
- set underblock ""
- set argsflags [lrange $args 0 end-1]
- } else {
- error "renderspace expects opt-val pairs followed by: or just "
- }
- }
- set opts [tcl::dict::create\
- -bias ignored\
- -width \uFFEF\
- -height \uFFEF\
- -startcolumn 1\
- -ellipsis 0\
- -ellipsistext $default_ellipsis_horizontal\
- -ellipsiswhitespace 0\
- -expand_right 0\
- -appendlines 1\
- -transparent 0\
- -exposed1 \uFFFD\
- -exposed2 \uFFFD\
- -experimental 0\
- -cp437 0\
- -looplimit \uFFEF\
- -crm_mode 0\
- -reverse_mode 0\
- -insert_mode 0\
- -wrap 0\
- -info 0\
- -console {stdin stdout stderr}\
- ]
- #expand_right is perhaps consistent with the idea of the page_size being allowed to grow horizontally..
- # it does not necessarily mean the viewport grows. (which further implies need for horizontal scrolling)
- # - it does need to be within some concept of terminal width - as columns must be addressable by ansi sequences.
- # - This implies the -width option value must grow if it is tied to the concept of renderspace terminal width! REVIEW.
- # - further implication is that if expand_right grows the virtual renderspace terminal width -
- # then some sort of reflow/rerender needs to be done for preceeding lines?
- # possibly not - as expand_right is distinct from a normal terminal-width change event,
- # expand_right being primarily to support other operations such as textblock::table
-
- #todo - viewport width/height as separate concept to terminal width/height?
- #-ellipsis args not used if -wrap is true
- foreach {k v} $argsflags {
- switch -- $k {
- -looplimit - -width - -height - -startcolumn - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace
- - -transparent - -exposed1 - -exposed2 - -experimental
- - -expand_right - -appendlines
- - -reverse_mode - -crm_mode - -insert_mode
- - -cp437
- - -info - -console {
- tcl::dict::set opts $k $v
- }
- -wrap - -autowrap_mode {
- #temp alias -autowrap_mode for consistency with renderline
- #todo -
- tcl::dict::set opts -wrap $v
- }
- default {
- error "overtype::renderspace unknown option '$k'. Known options: [tcl::dict::keys $opts]"
- }
- }
- }
- #set opts [tcl::dict::merge $defaults $argsflags]
- # -- --- --- --- --- ---
- #review - expand_left for RTL text?
- set opt_expand_right [tcl::dict::get $opts -expand_right]
- #for repl - standard output line indicator is a dash - todo, add a different indicator for a continued line.
- set opt_width [tcl::dict::get $opts -width]
- set opt_height [tcl::dict::get $opts -height]
- set opt_startcolumn [tcl::dict::get $opts -startcolumn]
- set opt_appendlines [tcl::dict::get $opts -appendlines]
- set opt_transparent [tcl::dict::get $opts -transparent]
- set opt_ellipsistext [tcl::dict::get $opts -ellipsistext]
- set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace]
- set opt_exposed1 [tcl::dict::get $opts -exposed1] ;#widechar_exposed_left - todo
- set opt_exposed2 [tcl::dict::get $opts -exposed2] ;#widechar_exposed_right - todo
- # -- --- --- --- --- ---
- set opt_crm_mode [tcl::dict::get $opts -crm_mode]
- set opt_reverse_mode [tcl::dict::get $opts -reverse_mode]
- set opt_insert_mode [tcl::dict::get $opts -insert_mode]
- #####
- # review -wrap should map onto DECAWM terminal mode - the wrap 2 idea may not fit in with this?.
- set opt_autowrap_mode [tcl::dict::get $opts -wrap]
- #??? wrap 1 is hard wrap cutting word at exact column, or 1 column early for 2w-glyph, wrap 2 is for language-based word-wrap algorithm (todo)
- #####
- # -- --- --- --- --- ---
- set opt_cp437 [tcl::dict::get $opts -cp437]
- set opt_info [tcl::dict::get $opts -info]
-
-
-
- # ----------------------------
- # -experimental dev flag to set flags etc
- # ----------------------------
- set data_mode 0
- set edit_mode 0
- set opt_experimental [tcl::dict::get $opts -experimental]
- foreach o $opt_experimental {
- switch -- $o {
- data_mode {
- set data_mode 1
- }
- edit_mode {
- set edit_mode 1
- }
- }
- }
- # ----------------------------
-
-
- set underblock [tcl::string::map {\r\n \n} $underblock]
- set overblock [tcl::string::map {\r\n \n} $overblock]
-
-
- #set underlines [split $underblock \n]
-
- #underblock is a 'rendered' block - so width height make sense
- #only non-cursor affecting and non-width occupying ANSI codes should be present.
- #ie SGR codes and perhaps things such as PM - although generally those should have been pushed to the application already
- #renderwidth & renderheight were originally used with reference to rendering into a 'column' of output e.g a table column - before cursor row/col was implemented.
-
- if {$opt_width eq "\uFFEF" || $opt_height eq "\uFFEF"} {
- lassign [blocksize $underblock] _w renderwidth _h renderheight
- if {$opt_width ne "\uFFEF"} {
- set renderwidth $opt_width
- }
- if {$opt_height ne "\uFFEF"} {
- set renderheight $opt_height
- }
- } else {
- set renderwidth $opt_width
- set renderheight $opt_height
- }
- #initial state for renderspace 'terminal' reset
- set initial_state [dict create\
- renderwidth $renderwidth\
- renderheight $renderheight\
- crm_mode $opt_crm_mode\
- reverse_mode $opt_reverse_mode\
- insert_mode $opt_insert_mode\
- autowrap_mode $opt_autowrap_mode\
- cp437 $opt_cp437\
- ]
- #modes
- #e.g insert_mode can be toggled by insert key or ansi IRM sequence CSI 4 h|l
- #opt_startcolumn ?? - DECSLRM ?
- set vtstate $initial_state
-
- # -- --- --- ---
- #REVIEW - do we need ansi resets in the underblock?
- if {$underblock eq ""} {
- set underlines [lrepeat $renderheight ""]
- } else {
- set underblock [textblock::join_basic -- $underblock] ;#ensure properly rendered - ansi per-line resets & replays
- set underlines [split $underblock \n]
- }
- #if {$underblock eq ""} {
- # set blank "\x1b\[0m\x1b\[0m"
- # #set underlines [list "\x1b\[0m\x1b\[0m"]
- # set underlines [lrepeat $renderheight $blank]
- #} else {
- # #lines_as_list -ansiresets 1 will do nothing if -ansiresets 1 isn't specified - REVIEW
- # set underlines [lines_as_list -ansiresets 1 $underblock]
- #}
- # -- --- --- ---
-
- #todo - reconsider the 'line' as the natural chunking mechanism for the overlay.
- #In practice an overlay ANSI stream can be a single line with ansi moves/restores etc - or even have no moves or newlines, just relying on wrapping at the output renderwidth
- #In such cases - we process the whole shebazzle for the first output line - only reducing by the applied amount at the head each time, reprocessing the long tail each time.
- #(in cases where there are interline moves or cursor jumps anyway)
- #This works - but doesn't seem efficient.
- #On the other hand.. maybe it depends on the data. For simpler files it's more efficient than splitting first
-
- #a hack until we work out how to avoid infinite loops...
- #
- set looplimit [tcl::dict::get $opts -looplimit]
- if {$looplimit eq "\uFFEF"} {
- #looping for each char is worst case (all newlines?) - anything over that is an indication of something broken?
- #do we need any margin above the length? (telnet mapscii.me test)
- set looplimit [expr {[tcl::string::length $overblock] + 10}]
- }
-
- #overblock height/width isn't useful in the presence of an ansi input overlay with movements. The number of lines may bear little relationship to the output height
- #lassign [blocksize $overblock] _w overblock_width _h overblock_height
-
- set scheme 4
- switch -- $scheme {
- 0 {
- #one big chunk
- set inputchunks [list $overblock]
- }
- 1 {
- set inputchunks [punk::ansi::ta::split_codes $overblock]
- }
- 2 {
-
- #split into lines if possible first - then into plaintext/ansi-sequence chunks ?
- set inputchunks [list ""] ;#put an empty plaintext split in for starters
- set i 1
- set lines [split $overblock \n]
- foreach ln $lines {
- if {$i < [llength $lines]} {
- append ln \n
- }
- set sequence_split [punk::ansi::ta::split_codes_single $ln] ;#use split_codes Not split_codes_single?
- set lastpt [lindex $inputchunks end]
- lset inputchunks end [tcl::string::cat $lastpt [lindex $sequence_split 0]]
- lappend inputchunks {*}[lrange $sequence_split 1 end]
- incr i
- }
- }
- 3 {
- #it turns out line based chunks are faster than the above.. probably because some of those end up doing the regex splitting twice
- set lflines [list]
- set inputchunks [split $overblock \n]
- foreach ln $inputchunks {
- append ln \n
- lappend lflines $ln
- }
- if {[llength $lflines]} {
- lset lflines end [tcl::string::range [lindex $lflines end] 0 end-1]
- }
- #set inputchunks $lflines[unset lflines]
- set inputchunks [lindex [list $lflines [unset lflines]] 0]
-
- }
- 4 {
- set inputchunks [list]
- foreach ln [split $overblock \n] {
- lappend inputchunks $ln\n
- }
- if {[llength $inputchunks]} {
- lset inputchunks end [tcl::string::range [lindex $inputchunks end] 0 end-1]
- }
- }
- }
-
-
-
-
- set replay_codes_underlay [tcl::dict::create 1 ""]
- #lappend replay_codes_overlay ""
- set replay_codes_overlay "[punk::ansi::a]"
- set unapplied ""
- set cursor_saved_position [tcl::dict::create]
- set cursor_saved_attributes ""
-
-
- set outputlines $underlines
- set overidx 0
-
- #underlines are not necessarily processed in order - depending on cursor-moves applied from overtext
- set row 1
- #if {$data_mode} {
- # set col [_get_row_append_column $row]
- #} else {
- set col $opt_startcolumn
- #}
-
- set instruction_stats [tcl::dict::create]
-
- set loop 0
- #while {$overidx < [llength $inputchunks]} { }
-
- while {[llength $inputchunks]} {
- #set overtext [lindex $inputchunks $overidx]; lset inputchunks $overidx ""
- set overtext [lpop inputchunks 0]
- if {![tcl::string::length $overtext]} {
- incr loop
- continue
- }
- #puts "----->[ansistring VIEW -lf 1 -vt 1 -nul 1 $overtext]<----"
- set undertext [lindex $outputlines [expr {$row -1}]]
- set renderedrow $row
-
- #renderline pads each underaly line to width with spaces and should track where end of data is
-
-
- #set overtext [tcl::string::cat [lindex $replay_codes_overlay $overidx] $overtext]
- set overtext $replay_codes_overlay$overtext
- if {[tcl::dict::exists $replay_codes_underlay $row]} {
- set undertext [tcl::dict::get $replay_codes_underlay $row]$undertext
- }
- #review insert_mode. As an 'overtype' function whose main function is not interactive keystrokes - insert is secondary -
- #but even if we didn't want it as an option to the function call - to process ansi adequately we need to support IRM (insertion-replacement mode) ESC [ 4 h|l
- set renderopts [list -experimental $opt_experimental\
- -cp437 $opt_cp437\
- -info 1\
- -crm_mode [tcl::dict::get $vtstate crm_mode]\
- -insert_mode [tcl::dict::get $vtstate insert_mode]\
- -autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\
- -reverse_mode [tcl::dict::get $vtstate reverse_mode]\
- -cursor_restore_attributes $cursor_saved_attributes\
- -transparent $opt_transparent\
- -width [tcl::dict::get $vtstate renderwidth]\
- -exposed1 $opt_exposed1\
- -exposed2 $opt_exposed2\
- -expand_right $opt_expand_right\
- -cursor_column $col\
- -cursor_row $row\
- ]
- set rinfo [renderline {*}$renderopts $undertext $overtext]
-
- set instruction [tcl::dict::get $rinfo instruction]
- tcl::dict::set vtstate crm_mode [tcl::dict::get $rinfo crm_mode]
- tcl::dict::set vtstate insert_mode [tcl::dict::get $rinfo insert_mode]
- tcl::dict::set vtstate autowrap_mode [tcl::dict::get $rinfo autowrap_mode] ;#
- tcl::dict::set vtstate reverse_mode [tcl::dict::get $rinfo reverse_mode]
- #how to support reverse_mode in rendered linelist? we need to examine all pt/code blocks and flip each SGR stack?
- # - review - the answer is probably that we don't need to - it is set/reset only during application of overtext
-
- #Note carefully the difference betw overflow_right and unapplied.
- #overflow_right may need to be included in next run before the unapplied data
- #overflow_right most commonly has data when in insert_mode
- set rendered [tcl::dict::get $rinfo result]
- set overflow_right [tcl::dict::get $rinfo overflow_right]
- set overflow_right_column [tcl::dict::get $rinfo overflow_right_column]
- set unapplied [tcl::dict::get $rinfo unapplied]
- set unapplied_list [tcl::dict::get $rinfo unapplied_list]
- set post_render_col [tcl::dict::get $rinfo cursor_column]
- set post_render_row [tcl::dict::get $rinfo cursor_row]
- set c_saved_pos [tcl::dict::get $rinfo cursor_saved_position]
- set c_saved_attributes [tcl::dict::get $rinfo cursor_saved_attributes]
- set visualwidth [tcl::dict::get $rinfo visualwidth] ;#column width of what is 'rendered' for the line
- set insert_lines_above [tcl::dict::get $rinfo insert_lines_above]
- set insert_lines_below [tcl::dict::get $rinfo insert_lines_below]
- tcl::dict::set replay_codes_underlay [expr {$renderedrow+1}] [tcl::dict::get $rinfo replay_codes_underlay]
-
- #lset replay_codes_overlay [expr $overidx+1] [tcl::dict::get $rinfo replay_codes_overlay]
- set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay]
- if {0 && [tcl::dict::get $vtstate reverse_mode]} {
- #test branch - todo - prune
- puts stderr "---->[ansistring VIEW $replay_codes_overlay] rendered: $rendered"
- #review
- #JMN3
- set existing_reverse_state 0
- #split_codes_single is single esc sequence - but could have multiple sgr codes within one esc sequence
- #e.g \x1b\[0;31;7m has a reset,colour red and reverse
- set codeinfo [punk::ansi::codetype::sgr_merge [list $replay_codes_overlay] -info 1]
- set codestate_reverse [dict get $codeinfo codestate reverse]
- switch -- $codestate_reverse {
- 7 {
- set existing_reverse_state 1
- }
- 27 {
- set existing_reverse_state 0
- }
- "" {
- }
- }
- if {$existing_reverse_state == 0} {
- set rflip [a+ reverse]
- } else {
- #reverse of reverse
- set rflip [a+ noreverse]
- }
- #note that mergeresult can have multiple esc (due to unmergeables or non sgr codes)
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge [list [dict get $codeinfo mergeresult] $rflip]]
- puts stderr "---->[ansistring VIEW $replay_codes_overlay] rendered: $rendered"
- }
-
-
-
- #-- todo - detect looping properly
- if {$row > 1 && $overtext ne "" && $unapplied eq $overtext && $post_render_row == $row && $instruction eq ""} {
- puts stderr "overtype::renderspace loop?"
- puts [ansistring VIEW $rinfo]
- break
- }
- #--
-
- if {[tcl::dict::size $c_saved_pos] >= 1} {
- set cursor_saved_position $c_saved_pos
- set cursor_saved_attributes $c_saved_attributes
- }
-
-
- set overflow_handled 0
-
-
-
- set nextprefix ""
-
-
- #todo - handle potential insertion mode as above for cursor restore?
- #keeping separate branches for debugging - review and merge as appropriate when stable
- set instruction_type [lindex $instruction 0] ;#some instructions have params
- tcl::dict::incr instruction_stats $instruction_type
- switch -- $instruction_type {
- reset {
- #reset the 'renderspace terminal' (not underlying terminal)
- set row 1
- set col 1
- set vtstate [tcl::dict::merge $vtstate $initial_state]
- #todo - clear screen
- }
- {} {
- #end of supplied line input
- #lf included in data
- set row $post_render_row
- set col $post_render_col
- if {![llength $unapplied_list]} {
- if {$overflow_right ne ""} {
- incr row
- }
- } else {
- puts stderr "renderspace end of input line - has unapplied: [ansistring VIEW $unapplied] (review)"
- }
- set col $opt_startcolumn
- }
- up {
-
- #renderline knows it's own line number, and knows not to go above row l
- #it knows that a move whilst 1-beyond the width conflicts with the linefeed and reduces the move by one accordingly.
- #row returned should be correct.
- #column may be the overflow column - as it likes to report that to the caller.
-
- #Note that an ansi up sequence after last column going up to a previous line and also beyond the last column, will result in the next grapheme going onto the following line.
- #this seems correct - as the column remains beyond the right margin so subsequent chars wrap (?) review
- #puts stderr "up $post_render_row"
- #puts stderr "$rinfo"
-
- #puts stdout "1 row:$row col $col"
- set row $post_render_row
- #data_mode (naming?) determines if we move to end of existing data or not.
- #data_mode 0 means ignore existing line length and go to exact column
- #set by -experimental flag
- if {$data_mode == 0} {
- set col $post_render_col
- } else {
- #This doesn't really work if columns are pre-filled with spaces..we can't distinguish them from data
- #we need renderline to return the number of the maximum column filled (or min if we ever do r-to-l)
- set existingdata [lindex $outputlines [expr {$post_render_row -1}]]
- set lastdatacol [punk::ansi::printing_length $existingdata]
- if {$lastdatacol < $renderwidth} {
- set col [expr {$lastdatacol+1}]
- } else {
- set col $renderwidth
- }
- }
-
- #puts stdout "2 row:$row col $col"
- #puts stdout "-----------------------"
- #puts stdout $rinfo
- #flush stdout
- }
- down {
- if {$data_mode == 0} {
- #renderline doesn't know how far down we can go..
- if {$post_render_row > [llength $outputlines]} {
- if {$opt_appendlines} {
- set diff [expr {$post_render_row - [llength $outputlines]}]
- if {$diff > 0} {
- lappend outputlines {*}[lrepeat $diff ""]
- }
- lappend outputlines ""
- }
- }
- set row $post_render_row
- set col $post_render_col
- } else {
- if {$post_render_row > [llength $outputlines]} {
- if {$opt_appendlines} {
- set diff [expr {$post_render_row - [llength $outputlines]}]
- if {$diff > 0} {
- lappend outputlines {*}[lrepeat $diff ""]
- }
- lappend outputlines ""
- }
- }
- set existingdata [lindex $outputlines [expr {$post_render_row -1}]]
- set lastdatacol [punk::ansi::printing_length $existingdata]
- if {$lastdatacol < $renderwidth} {
- set col [expr {$lastdatacol+1}]
- } else {
- set col $renderwidth
- }
-
- }
- }
- restore_cursor {
- #testfile belinda.ans uses this
-
- #puts stdout "[a+ blue bold]CURSOR_RESTORE[a]"
- if {[tcl::dict::exists $cursor_saved_position row]} {
- set row [tcl::dict::get $cursor_saved_position row]
- set col [tcl::dict::get $cursor_saved_position column]
- #puts stdout "restoring: row $row col $col [ansistring VIEW $cursor_saved_attributes] [a] unapplied [ansistring VIEWCODES $unapplied]"
- #set nextprefix $cursor_saved_attributes
- #lset replay_codes_overlay [expr $overidx+1] $cursor_saved_attributes
- set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay]$cursor_saved_attributes
- #set replay_codes_overlay $cursor_saved_attributes
- set cursor_saved_position [tcl::dict::create]
- set cursor_saved_attributes ""
- } else {
- #TODO
- #?restore without save?
- #should move to home position and reset ansi SGR?
- #puts stderr "overtype::renderspace cursor_restore without save data available"
- }
- #If we were inserting prior to hitting the cursor_restore - there could be overflow_right data - generally the overtype functions aren't for inserting - but ansi can enable it
- #if we were already in overflow when cursor_restore was hit - it shouldn't have been processed as an action - just stored.
- if {!$overflow_handled && $overflow_right ne ""} {
- #wrap before restore? - possible effect on saved cursor position
- #this overflow data has previously been rendered so has no cursor movements or further save/restore operations etc
- #we can just insert another call to renderline to solve this.. ?
- #It would perhaps be more properly handled as a queue of instructions from our initial renderline call
- #we don't need to worry about overflow next call (?)- but we should carry forward our gx and ansi stacks
-
- puts stdout ">>>[a+ red bold]overflow_right during restore_cursor[a]"
-
- set sub_info [overtype::renderline\
- -info 1\
- -width [tcl::dict::get $vtstate renderwidth]\
- -insert_mode [tcl::dict::get $vtstate insert_mode]\
- -autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\
- -expand_right [tcl::dict::get $opts -expand_right]\
- ""\
- $overflow_right\
- ]
- set foldline [tcl::dict::get $sub_info result]
- tcl::dict::set vtstate insert_mode [tcl::dict::get $sub_info insert_mode] ;#probably not needed..?
- tcl::dict::set vtstate autowrap_mode [tcl::dict::get $sub_info autowrap_mode] ;#nor this..
- linsert outputlines $renderedrow $foldline
- #review - row & col set by restore - but not if there was no save..
- }
- set overflow_handled 1
-
- }
- move {
- ########
- if {$post_render_row > [llength $outputlines]} {
- #Ansi moves need to create new lines ?
- #if {$opt_appendlines} {
- # set diff [expr {$post_render_row - [llength $outputlines]}]
- # if {$diff > 0} {
- # lappend outputlines {*}[lrepeat $diff ""]
- # }
- # set row $post_render_row
- #} else {
- set row [llength $outputlines]
- #}
- } else {
- set row $post_render_row
- }
- #######
- set col $post_render_col
- #overflow + unapplied?
- }
- clear_and_move {
- #e.g 2J
- if {$post_render_row > [llength $outputlines]} {
- set row [llength $outputlines]
- } else {
- set row $post_render_row
- }
- set col $post_render_col
- set overflow_right "" ;#if we're clearing - any overflow due to insert_mode is irrelevant
- set clearedlines [list]
- foreach ln $outputlines {
- lappend clearedlines \x1b\[0m$replay_codes_overlay[string repeat \000 $renderwidth]\x1b\[0m
- if 0 {
-
- set lineparts [punk::ansi::ta::split_codes $ln]
- set numcells 0
- foreach {pt _code} $lineparts {
- if {$pt ne ""} {
- foreach grapheme [punk::char::grapheme_split $pt] {
- switch -- $grapheme {
- " " - - - _ - ! - @ - # - $ - % - ^ - & - * - = - + - : - . - , - / - | - ? -
- a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y -
- z - A - B - C - D - E - F - G - H - I - J - K - L - M - N - O - P - Q - R - S - T - U - V - W - X - Y - Z - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 {
- incr numcells 1
- }
- default {
- if {$grapheme eq "\u0000"} {
- incr numcells 1
- } else {
- incr numcells [grapheme_width_cached $grapheme]
- }
- }
- }
-
- }
- }
- }
- #replays/resets each line
- lappend clearedlines \x1b\[0m$replay_codes_overlay[string repeat \000 $numcells]\x1b\[0m
- }
- }
- set outputlines $clearedlines
- #todo - determine background/default to be in effect - DECECM ?
- puts stderr "replay_codes_overlay: [ansistring VIEW $replay_codes_overlay]"
- #lset outputlines 0 $replay_codes_overlay[lindex $outputlines 0]
-
- }
- lf_start {
- #raw newlines
- # ----------------------
- #test with fruit.ans
- #test - treating as newline below...
- #append rendered $overflow_right
- #set overflow_right ""
- set row $renderedrow
- incr row
- if {$row > [llength $outputlines]} {
- lappend outputlines ""
- }
- set col $opt_startcolumn
- # ----------------------
- }
- lf_mid {
-
- set edit_mode 0
- if {$edit_mode} {
- set inputchunks [linsert $inputchunks 0 $overflow_right$unapplied]
- set overflow_right ""
- set unapplied ""
- set row $post_render_row
- #set col $post_render_col
- set col $opt_startcolumn
- if {$row > [llength $outputlines]} {
- lappend outputlines {*}[lrepeat 1 ""]
- }
- } else {
- if 1 {
- if {$overflow_right ne ""} {
- if {$opt_expand_right} {
- append rendered $overflow_right
- set overflow_right ""
- } else {
- #review - we should really make renderline do the work...?
- set overflow_width [punk::ansi::printing_length $overflow_right]
- if {$visualwidth + $overflow_width <= $renderwidth} {
- append rendered $overflow_right
- set overflow_right ""
- } else {
- if {[tcl::dict::get $vtstate autowrap_mode]} {
- set outputlines [linsert $outputlines $renderedrow $overflow_right]
- set overflow_right ""
- set row [expr {$renderedrow + 2}]
- } else {
- set overflow_right "" ;#abandon
- }
-
- if {0 && $visualwidth < $renderwidth} {
- puts stderr "visualwidth: $visualwidth < renderwidth:$renderwidth"
- error "incomplete - abandon?"
- set overflowparts [punk::ansi::ta::split_codes $overflow_right]
- set remaining_overflow $overflowparts
- set filled 0
- foreach {pt code} $overflowparts {
- lpop remaining_overflow 0
- if {$pt ne ""} {
- set graphemes [punk::char::grapheme_split $pt]
- set add ""
- set addlen $visualwidth
- foreach g $graphemes {
- set w [overtype::grapheme_width_cached $g]
- if {$addlen + $w <= $renderwidth} {
- append add $g
- incr addlen $w
- } else {
- set filled 1
- break
- }
- }
- append rendered $add
- }
- if {!$filled} {
- lpop remaining_overflow 0 ;#pop code
- }
- }
- set overflow_right [join $remaining_overflow ""]
- }
- }
- }
- }
- set row $post_render_row
- set col $opt_startcolumn
- if {$row > [llength $outputlines]} {
- lappend outputlines {*}[lrepeat 1 ""]
- }
- } else {
- #old version - known to work with various ansi graphics - e.g fruit.ans
- # - but fails to limit lines to renderwidth when expand_right == 0
- append rendered $overflow_right
- set overflow_right ""
- set row $post_render_row
- set col $opt_startcolumn
- if {$row > [llength $outputlines]} {
- lappend outputlines {*}[lrepeat 1 ""]
- }
- }
- }
- }
- lf_overflow {
- #linefeed after renderwidth e.g at column 81 for an 80 col width
- #we may also have other control sequences that came after col 80 e.g cursor save
-
- if 0 {
- set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -nul 1 -lf 1 -vt 1 $rendered]]
- set lhs [textblock::frame -title "rendered $visualwidth cols" -subtitle "row-$renderedrow" $lhs]
- set rhs ""
-
- #assertion - there should be no overflow..
- puts $lhs
- }
- if {![tcl::dict::get $vtstate insert_mode]} {
- assert {$overflow_right eq ""} lf_overflow should not get data in overflow_right when not insert_mode
- }
-
- set row $post_render_row
- #set row $renderedrow
- #incr row
- #only add newline if we're at the bottom
- if {$row > [llength $outputlines]} {
- lappend outputlines {*}[lrepeat 1 ""]
- }
- set col $opt_startcolumn
-
- }
- newlines_above {
- #we get a newlines_above instruction when received at column 1
- #In some cases we want to treat that as request to insert a new blank line above, and move our row 1 down (staying with the data)
- #in other cases - we want to treat at column 1 the same as any other
-
- puts "--->newlines_above"
- puts "rinfo: $rinfo"
- #renderline doesn't advance the row for us - the caller has the choice to implement or not
- set row $post_render_row
- set col $post_render_col
- if {$insert_lines_above > 0} {
- set row $renderedrow
- set outputlines [linsert $outputlines $renderedrow-1 {*}[lrepeat $insert_lines_above ""]]
- incr row [expr {$insert_lines_above -1}] ;#we should end up on the same line of text (at a different index), with new empties inserted above
- #? set row $post_render_row #can renderline tell us?
- }
- }
- newlines_below {
- #obsolete? - use for ANSI insert lines sequence
- if {$data_mode == 0} {
- puts --->nl_below
- set row $post_render_row
- set col $post_render_col
- if {$insert_lines_below == 1} {
- #set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -lf 1 -vt 1 $rendered]]
- #set lhs [textblock::frame -title rendered -subtitle "row-$renderedrow" $lhs]
- #set rhs ""
- #if {$overflow_right ne ""} {
- # set rhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -lf 1 -vt 1 $overflow_right]]
- # set rhs [textblock::frame -title overflow_right $rhs]
- #}
- #puts [textblock::join $lhs $rhs]
-
- #rendered
- append rendered $overflow_right
- #
-
-
- set overflow_right ""
- set row $renderedrow
- #only add newline if we're at the bottom
- if {$row > [llength $outputlines]} {
- lappend outputlines {*}[lrepeat $insert_lines_below ""]
- }
- incr row $insert_lines_below
- set col $opt_startcolumn
- }
- } else {
- set row $post_render_row
- if {$post_render_row > [llength $outputlines]} {
- if {$opt_appendlines} {
- set diff [expr {$post_render_row - [llength $outputlines]}]
- if {$diff > 0} {
- lappend outputlines {*}[lrepeat $diff ""]
- }
- lappend outputlines ""
- }
- } else {
- set existingdata [lindex $outputlines [expr {$post_render_row -1}]]
- set lastdatacol [punk::ansi::printing_length $existingdata]
- if {$lastdatacol < $renderwidth} {
- set col [expr {$lastdatacol+1}]
- } else {
- set col $renderwidth
- }
- }
- }
- }
- wrapmoveforward {
- #doesn't seem to be used by fruit.ans testfile
- #used by dzds.ans
- #note that cursor_forward may move deep into the next line - or even span multiple lines !TODO
- set c $renderwidth
- set r $post_render_row
- if {$post_render_col > $renderwidth} {
- set i $c
- while {$i <= $post_render_col} {
- if {$c == $renderwidth+1} {
- incr r
- if {$opt_appendlines} {
- if {$r < [llength $outputlines]} {
- lappend outputlines ""
- }
- }
- set c $opt_startcolumn
- } else {
- incr c
- }
- incr i
- }
- set col $c
- } else {
- #why are we getting this instruction then?
- puts stderr "wrapmoveforward - test"
- set r [expr {$post_render_row +1}]
- set c $post_render_col
- }
- set row $r
- set col $c
- }
- wrapmovebackward {
- set c $renderwidth
- set r $post_render_row
- if {$post_render_col < 1} {
- set c 1
- set i $c
- while {$i >= $post_render_col} {
- if {$c == 0} {
- if {$r > 1} {
- incr r -1
- set c $renderwidth
- } else {
- #leave r at 1 set c 1
- #testfile besthpav.ans first line top left border alignment
- set c 1
- break
- }
- } else {
- incr c -1
- }
- incr i -1
- }
- set col $c
- } else {
- puts stderr "Wrapmovebackward - but postrendercol >= 1???"
- }
- set row $r
- set col $c
- }
- overflow {
- #normal single-width grapheme overflow
- #puts "----normal overflow --- [ansistring VIEWSTYLE -lf 1 -nul 1 -vt 1 $rendered]"
- set row $post_render_row ;#renderline will not advance row when reporting overflow char
- if {[tcl::dict::get $vtstate autowrap_mode]} {
- incr row
- set col $opt_startcolumn ;#whether wrap or not - next data is at column 1 ??
- } else {
- set col $post_render_col
- #set unapplied "" ;#this seems wrong?
- #set unapplied [tcl::string::range $unapplied 1 end]
- #The overflow can only be triggered by a grapheme (todo cluster?) - but our unapplied could contain SGR codes prior to the grapheme that triggered overflow - so we need to skip beyond any SGRs
- #There may be more than one, because although the stack leading up to overflow may have been merged - codes between the last column and the overflowing grapheme will remain separate
- #We don't expect any movement or other ANSI codes - as if they came before the grapheme, they would have triggered a different instruction to 'overflow'
- set idx 0
- set next_grapheme_index -1
- foreach u $unapplied_list {
- if {![punk::ansi::ta::detect $u]} {
- set next_grapheme_index $idx
- break
- }
- incr idx
- }
- assert {$next_grapheme_index >= 0}
- #drop the overflow grapheme - keeping all codes in place.
- set unapplied [join [lreplace $unapplied_list $next_grapheme_index $next_grapheme_index] ""]
- #we need to run the reduced unapplied on the same line - further graphemes will just overflow again, but codes or control chars could trigger jumps to other lines
-
- set overflow_handled 1
- #handled by dropping overflow if any
- }
- }
- overflow_splitchar {
- set row $post_render_row ;#renderline will not advance row when reporting overflow char
-
- #2nd half of grapheme would overflow - treggering grapheme is returned in unapplied. There may also be overflow_right from earlier inserts
- #todo - consider various options .. re-render a single trailing space or placeholder on same output line, etc
- if {[tcl::dict::get $vtstate autowrap_mode]} {
- if {$renderwidth < 2} {
- #edge case of rendering to a single column output - any 2w char will just cause a loop if we don't substitute with something, or drop the character
- set idx 0
- set triggering_grapheme_index -1
- foreach u $unapplied_list {
- if {![punk::ansi::ta::detect $u]} {
- set triggering_grapheme_index $idx
- break
- }
- incr idx
- }
- set unapplied [join [lreplace $unapplied_list $triggering_grapheme_index $triggering_grapheme_index $opt_exposed1] ""]
- } else {
- set col $opt_startcolumn
- incr row
- }
- } else {
- set overflow_handled 1
- #handled by dropping entire overflow if any
- if {$renderwidth < 2} {
- set idx 0
- set triggering_grapheme_index -1
- foreach u $unapplied_list {
- if {![punk::ansi::ta::detect $u]} {
- set triggering_grapheme_index $idx
- break
- }
- incr idx
- }
- set unapplied [join [lreplace $unapplied_list $triggering_grapheme_index $triggering_grapheme_index $opt_exposed1] ""]
- }
- }
-
- }
- vt {
-
- #can vt add a line like a linefeed can?
- set row $post_render_row
- set col $post_render_col
- }
- set_window_title {
- set newtitle [lindex $instruction 1]
- puts stderr "overtype::renderspace set_window_title [ansistring VIEW $newtitle] instruction '$instruction'"
- #
- }
- reset_colour_palette {
- puts stderr "overtype::renderspace instruction '$instruction' unimplemented"
- }
- default {
- puts stderr "overtype::renderspace unhandled renderline instruction '$instruction'"
- }
-
- }
-
-
- if {!$opt_expand_right && ![tcl::dict::get $vtstate autowrap_mode]} {
- #not allowed to overflow column or wrap therefore we get overflow data to truncate
- if {[tcl::dict::get $opts -ellipsis]} {
- set show_ellipsis 1
- if {!$opt_ellipsiswhitespace} {
- #we don't want ellipsis if only whitespace was lost
- set lostdata ""
- if {$overflow_right ne ""} {
- append lostdata $overflow_right
- }
- if {$unapplied ne ""} {
- append lostdata $unapplied
- }
- if {[tcl::string::trim $lostdata] eq ""} {
- set show_ellipsis 0
- }
- #set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end]
- if {[tcl::string::trim [punk::ansi::ansistrip $lostdata]] eq ""} {
- set show_ellipsis 0
- }
- }
- if {$show_ellipsis} {
- set rendered [overtype::right $rendered $opt_ellipsistext]
- }
- set overflow_handled 1
- } else {
- #no wrap - no ellipsis - silently truncate
- set overflow_handled 1
- }
- }
-
-
-
- if {$renderedrow <= [llength $outputlines]} {
- lset outputlines [expr {$renderedrow-1}] $rendered
- } else {
- if {$opt_appendlines} {
- lappend outputlines $rendered
- } else {
- #?
- lset outputlines [expr {$renderedrow-1}] $rendered
- }
- }
-
- if {!$overflow_handled} {
- append nextprefix $overflow_right
- }
-
- append nextprefix $unapplied
-
- if 0 {
- if {$nextprefix ne ""} {
- set nextoveridx [expr {$overidx+1}]
- if {$nextoveridx >= [llength $inputchunks]} {
- lappend inputchunks $nextprefix
- } else {
- #lset overlines $nextoveridx $nextprefix[lindex $overlines $nextoveridx]
- set inputchunks [linsert $inputchunks $nextoveridx $nextprefix]
- }
- }
- }
-
- if {$nextprefix ne ""} {
- set inputchunks [linsert $inputchunks 0 $nextprefix]
- }
-
-
- incr overidx
- incr loop
- if {$loop >= $looplimit} {
- puts stderr "overtype::renderspace looplimit reached ($looplimit)"
- lappend outputlines "[a+ red bold] - looplimit $looplimit reached[a]"
- set Y [a+ yellow bold]
- set RST [a]
- set sep_header ----DEBUG-----
- set debugmsg ""
- append debugmsg "${Y}${sep_header}${RST}" \n
- append debugmsg "looplimit $looplimit reached\n"
- append debugmsg "data_mode:$data_mode\n"
- append debugmsg "opt_appendlines:$opt_appendlines\n"
- append debugmsg "prev_row :[tcl::dict::get $renderopts -cursor_row]\n"
- append debugmsg "prev_col :[tcl::dict::get $renderopts -cursor_column]\n"
- tcl::dict::for {k v} $rinfo {
- append debugmsg "${Y}$k [ansistring VIEW -lf 1 -vt 1 $v]$RST" \n
- }
- append debugmsg "${Y}[string repeat - [string length $sep_header]]$RST" \n
-
- puts stdout $debugmsg
- #todo - config regarding error dumps rather than just dumping in working dir
- set fd [open [pwd]/error_overtype.txt w]
- puts $fd $debugmsg
- close $fd
- error $debugmsg
- break
- }
- }
-
- set result [join $outputlines \n]
- if {!$opt_info} {
- return $result
- } else {
- #emit to debug window like basictelnet does? make debug configurable as syslog or even a telnet server to allow on 2nd window?
- #append result \n$instruction_stats\n
- set inforesult [dict create\
- result $result\
- last_instruction $instruction\
- instruction_stats $instruction_stats\
- ]
- if {$opt_info == 2} {
- return [pdict -channel none inforesult]
- } else {
- return $inforesult
- }
- }
- }
-
- #todo - left-right ellipsis ?
- proc centre {args} {
- variable default_ellipsis_horizontal
- if {[llength $args] < 2} {
- error {usage: ?-transparent [0|1]? ?-bias [left|right]? ?-overflow [1|0]? undertext overtext}
- }
-
- foreach {underblock overblock} [lrange $args end-1 end] break
-
- #todo - vertical vs horizontal overflow for blocks
- set opts [tcl::dict::create\
- -bias left\
- -ellipsis 0\
- -ellipsistext $default_ellipsis_horizontal\
- -ellipsiswhitespace 0\
- -overflow 0\
- -transparent 0\
- -exposed1 \uFFFD\
- -exposed2 \uFFFD\
- ]
- set argsflags [lrange $args 0 end-2]
- foreach {k v} $argsflags {
- switch -- $k {
- -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 {
- tcl::dict::set opts $k $v
- }
- default {
- set known_opts [tcl::dict::keys $opts]
- error "overtype::centre unknown option '$k'. Known options: $known_opts"
- }
- }
- }
- #set opts [tcl::dict::merge $defaults $argsflags]
- # -- --- --- --- --- ---
- set opt_transparent [tcl::dict::get $opts -transparent]
- set opt_ellipsis [tcl::dict::get $opts -ellipsis]
- set opt_ellipsistext [tcl::dict::get $opts -ellipsistext]
- set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace]
- set opt_exposed1 [tcl::dict::get $opts -exposed1]
- set opt_exposed2 [tcl::dict::get $opts -exposed2]
- # -- --- --- --- --- ---
-
-
- set underblock [tcl::string::map {\r\n \n} $underblock]
- set overblock [tcl::string::map {\r\n \n} $overblock]
-
- set underlines [split $underblock \n]
- #set renderwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]]
- lassign [blocksize $underblock] _w renderwidth _h renderheight
- set overlines [split $overblock \n]
- lassign [blocksize $overblock] _w overblock_width _h overblock_height
- set under_exposed_max [expr {$renderwidth - $overblock_width}]
- if {$under_exposed_max > 0} {
- #background block is wider
- if {$under_exposed_max % 2 == 0} {
- #even left/right exposure
- set left_exposed [expr {$under_exposed_max / 2}]
- } else {
- set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division
- if {[tcl::string::tolower [tcl::dict::get $opts -bias]] eq "left"} {
- set left_exposed $beforehalf
- } else {
- #bias to the right
- set left_exposed [expr {$beforehalf + 1}]
- }
- }
- } else {
- set left_exposed 0
- }
-
- set outputlines [list]
- if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} {
- set replay_codes "[punk::ansi::a]"
- } else {
- set replay_codes ""
- }
- set replay_codes_underlay ""
- set replay_codes_overlay ""
- foreach undertext $underlines overtext $overlines {
- set overtext_datalen [punk::ansi::printing_length $overtext]
- set ulen [punk::ansi::printing_length $undertext]
- if {$ulen < $renderwidth} {
- set udiff [expr {$renderwidth - $ulen}]
- set undertext "$undertext[string repeat { } $udiff]"
- }
- set undertext $replay_codes_underlay$undertext
- set overtext $replay_codes_overlay$overtext
-
- set overflowlength [expr {$overtext_datalen - $renderwidth}]
- #review - right-to-left langs should elide on left! - extra option required
-
- if {$overflowlength > 0} {
- #overlay line wider or equal
- #review - we still expand_right for centred for now.. possibly should do something like -expand_leftright with ellipsis each end?
- set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -expand_right [tcl::dict::get $opts -overflow] -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext]
- set rendered [tcl::dict::get $rinfo result]
- set overflow_right [tcl::dict::get $rinfo overflow_right]
- set unapplied [tcl::dict::get $rinfo unapplied]
- #todo - get replay_codes from overflow_right instead of wherever it was truncated?
-
- #overlay line data is wider - trim if overflow not specified in opts - and overtype an ellipsis at right if it was specified
- if {![tcl::dict::get $opts -overflow]} {
- #lappend outputlines [tcl::string::range $overtext 0 [expr {$renderwidth - 1}]]
- #set overtext [tcl::string::range $overtext 0 $renderwidth-1 ]
- if {$opt_ellipsis} {
- set show_ellipsis 1
- if {!$opt_ellipsiswhitespace} {
- #we don't want ellipsis if only whitespace was lost
- #don't use tcl::string::range on ANSI data
- #set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end]
- set lostdata ""
- if {$overflow_right ne ""} {
- append lostdata $overflow_right
- }
- if {$unapplied ne ""} {
- append lostdata $unapplied
- }
- if {[tcl::string::trim $lostdata] eq ""} {
- set show_ellipsis 0
- }
- }
- if {$show_ellipsis} {
- set rendered [overtype::right $rendered $opt_ellipsistext]
- }
- }
- }
- lappend outputlines $rendered
- #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent $undertext $overtext]
- } else {
- #background block is wider than or equal to data for this line
- #lappend outputlines [renderline -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext]
- set rinfo [renderline -info 1 -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext]
- lappend outputlines [tcl::dict::get $rinfo result]
- }
- set replay_codes_underlay [tcl::dict::get $rinfo replay_codes_underlay]
- set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay]
- }
- return [join $outputlines \n]
- }
-
- #overtype::right is for a rendered ragged underblock and a rendered ragged overblock
- #ie we can determine the block width for bost by examining the lines and picking the longest.
- #
- proc right {args} {
- #NOT the same as align-right - which should be done to the overblock first if required
- variable default_ellipsis_horizontal
- # @d !todo - implement overflow, length checks etc
-
- if {[llength $args] < 2} {
- error {usage: ?-overflow [1|0]? ?-transparent 0|? undertext overtext}
- }
- foreach {underblock overblock} [lrange $args end-1 end] break
-
- set opts [tcl::dict::create\
- -bias ignored\
- -ellipsis 0\
- -ellipsistext $default_ellipsis_horizontal\
- -ellipsiswhitespace 0\
- -overflow 0\
- -transparent 0\
- -exposed1 \uFFFD\
- -exposed2 \uFFFD\
- -align "left"\
- ]
- set argsflags [lrange $args 0 end-2]
- tcl::dict::for {k v} $argsflags {
- switch -- $k {
- -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -align {
- tcl::dict::set opts $k $v
- }
- default {
- set known_opts [tcl::dict::keys $opts]
- error "overtype::centre unknown option '$k'. Known options: $known_opts"
- }
- }
- }
- #set opts [tcl::dict::merge $defaults $argsflags]
- # -- --- --- --- --- ---
- set opt_transparent [tcl::dict::get $opts -transparent]
- set opt_ellipsis [tcl::dict::get $opts -ellipsis]
- set opt_ellipsistext [tcl::dict::get $opts -ellipsistext]
- set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace]
- set opt_overflow [tcl::dict::get $opts -overflow]
- set opt_exposed1 [tcl::dict::get $opts -exposed1]
- set opt_exposed2 [tcl::dict::get $opts -exposed2]
- set opt_align [tcl::dict::get $opts -align]
- # -- --- --- --- --- ---
-
- set underblock [tcl::string::map {\r\n \n} $underblock]
- set overblock [tcl::string::map {\r\n \n} $overblock]
-
- set underlines [split $underblock \n]
- lassign [blocksize $underblock] _w renderwidth _h renderheight
- set overlines [split $overblock \n]
- #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]]
- lassign [blocksize $overblock] _w overblock_width _h overblock_height
- set under_exposed_max [expr {max(0,$renderwidth - $overblock_width)}]
- set left_exposed $under_exposed_max
-
-
-
- set outputlines [list]
- if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} {
- set replay_codes "[punk::ansi::a]"
- } else {
- set replay_codes ""
- }
- set replay_codes_underlay ""
- set replay_codes_overlay ""
- foreach undertext $underlines overtext $overlines {
- set overtext_datalen [punk::ansi::printing_length $overtext]
- set ulen [punk::ansi::printing_length $undertext]
- if {$ulen < $renderwidth} {
- set udiff [expr {$renderwidth - $ulen}]
- #puts xxx
- append undertext [string repeat { } $udiff]
- }
- if {$overtext_datalen < $overblock_width} {
- set odiff [expr {$overblock_width - $overtext_datalen}]
- switch -- $opt_align {
- left {
- set startoffset 0
- }
- right {
- set startoffset $odiff
- }
- default {
- set half [expr {$odiff / 2}]
- #set lhs [string repeat { } $half]
- #set righthalf [expr {$odiff - $half}] ;#remainder - may be one more - so we are biased left
- #set rhs [string repeat { } $righthalf]
- set startoffset $half
- }
- }
- } else {
- set startoffset 0 ;#negative?
- }
-
- set undertext $replay_codes_underlay$undertext
- set overtext $replay_codes_overlay$overtext
-
- set overflowlength [expr {$overtext_datalen - $renderwidth}]
- if {$overflowlength > 0} {
- #raw overtext wider than undertext column
- set rinfo [renderline\
- -info 1\
- -insert_mode 0\
- -transparent $opt_transparent\
- -exposed1 $opt_exposed1 -exposed2 $opt_exposed2\
- -overflow $opt_overflow\
- -startcolumn [expr {1 + $startoffset}]\
- $undertext $overtext]
- set replay_codes [tcl::dict::get $rinfo replay_codes]
- set rendered [tcl::dict::get $rinfo result]
- if {!$opt_overflow} {
- if {$opt_ellipsis} {
- set show_ellipsis 1
- if {!$opt_ellipsiswhitespace} {
- #we don't want ellipsis if only whitespace was lost
- set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end]
- if {[tcl::string::trim $lostdata] eq ""} {
- set show_ellipsis 0
- }
- }
- if {$show_ellipsis} {
- set ellipsis $replay_codes$opt_ellipsistext
- #todo - overflow on left if allign = right??
- set rendered [overtype::right $rendered $ellipsis]
- }
- }
- }
- lappend outputlines $rendered
- } else {
- #padded overtext
- #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent -startcolumn [expr {$left_exposed + 1}] $undertext $overtext]
- #Note - we still need overflow(exapnd_right) here - as although the overtext is short - it may oveflow due to the startoffset
- set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -expand_right $opt_overflow -startcolumn [expr {$left_exposed + 1 + $startoffset}] $undertext $overtext]
- lappend outputlines [tcl::dict::get $rinfo result]
- }
- set replay_codes [tcl::dict::get $rinfo replay_codes]
- set replay_codes_underlay [tcl::dict::get $rinfo replay_codes_underlay]
- set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay]
- }
-
- return [join $outputlines \n]
- }
-
- proc left {args} {
- overtype::block -blockalign left {*}$args
- }
- #overtype a (possibly ragged) underblock with a (possibly ragged) overblock
- proc block {args} {
- variable default_ellipsis_horizontal
- # @d !todo - implement overflow, length checks etc
-
- if {[llength $args] < 2} {
- error {usage: ?-blockalign left|centre|right? ?-textalign left|centre|right? ?-overflow [1|0]? ?-transparent 0|? undertext overtext}
- }
- #foreach {underblock overblock} [lrange $args end-1 end] break
- lassign [lrange $args end-1 end] underblock overblock
-
- set opts [tcl::dict::create\
- -ellipsis 0\
- -ellipsistext $default_ellipsis_horizontal\
- -ellipsiswhitespace 0\
- -overflow 0\
- -transparent 0\
- -exposed1 \uFFFD\
- -exposed2 \uFFFD\
- -textalign "left"\
- -textvertical "top"\
- -blockalign "left"\
- -blockalignbias left\
- -blockvertical "top"\
- ]
- set argsflags [lrange $args 0 end-2]
- tcl::dict::for {k v} $argsflags {
- switch -- $k {
- -blockalignbias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -textalign - -blockalign - -blockvertical {
- tcl::dict::set opts $k $v
- }
- default {
- error "overtype::block unknown option '$k'. Known options: [tcl::dict::keys $opts]"
- }
- }
- }
- # -- --- --- --- --- ---
- set opt_transparent [tcl::dict::get $opts -transparent]
- set opt_ellipsis [tcl::dict::get $opts -ellipsis]
- set opt_ellipsistext [tcl::dict::get $opts -ellipsistext]
- set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace]
- set opt_overflow [tcl::dict::get $opts -overflow]
- set opt_exposed1 [tcl::dict::get $opts -exposed1]
- set opt_exposed2 [tcl::dict::get $opts -exposed2]
- set opt_textalign [tcl::dict::get $opts -textalign]
- set opt_blockalign [tcl::dict::get $opts -blockalign]
- if {$opt_blockalign eq "center"} {
- set opt_blockalign "centre"
- }
- # -- --- --- --- --- ---
-
- set underblock [tcl::string::map {\r\n \n} $underblock]
- set overblock [tcl::string::map {\r\n \n} $overblock]
-
- set underlines [split $underblock \n]
- lassign [blocksize $underblock] _w renderwidth _h renderheight
- set overlines [split $overblock \n]
- #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]]
- lassign [blocksize $overblock] _w overblock_width _h overblock_height
- set under_exposed_max [expr {max(0,$renderwidth - $overblock_width)}]
-
- switch -- $opt_blockalign {
- left {
- set left_exposed 0
- }
- right {
- set left_exposed $under_exposed_max
- }
- centre {
- if {$under_exposed_max > 0} {
- #background block is wider
- if {$under_exposed_max % 2 == 0} {
- #even left/right exposure
- set left_exposed [expr {$under_exposed_max / 2}]
- } else {
- set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division
- if {[tcl::string::tolower [tcl::dict::get $opts -blockalignbias]] eq "left"} {
- set left_exposed $beforehalf
- } else {
- #bias to the right
- set left_exposed [expr {$beforehalf + 1}]
- }
- }
- } else {
- set left_exposed 0
- }
- }
- default {
- set left_exposed 0
- }
- }
-
-
-
- set outputlines [list]
- if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} {
- set replay_codes "[punk::ansi::a]"
- } else {
- set replay_codes ""
- }
- set replay_codes_underlay ""
- set replay_codes_overlay ""
- foreach undertext $underlines overtext $overlines {
- set overtext_datalen [punk::ansi::printing_length $overtext]
- set ulen [punk::ansi::printing_length $undertext]
- if {$ulen < $renderwidth} {
- set udiff [expr {$renderwidth - $ulen}]
- #puts xxx
- append undertext [string repeat { } $udiff]
- }
- if {$overtext_datalen < $overblock_width} {
- set odiff [expr {$overblock_width - $overtext_datalen}]
- switch -- $opt_textalign {
- left {
- set startoffset 0
- }
- right {
- set startoffset $odiff
- }
- default {
- set half [expr {$odiff / 2}]
- #set lhs [string repeat { } $half]
- #set righthalf [expr {$odiff - $half}] ;#remainder - may be one more - so we are biased left
- #set rhs [string repeat { } $righthalf]
- set startoffset $half
- }
- }
- } else {
- set startoffset 0 ;#negative?
- }
-
- set undertext $replay_codes_underlay$undertext
- set overtext $replay_codes_overlay$overtext
-
- set overflowlength [expr {$overtext_datalen - $renderwidth}]
- if {$overflowlength > 0} {
- #raw overtext wider than undertext column
- set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -expand_right $opt_overflow -startcolumn [expr {1 + $startoffset}] $undertext $overtext]
- set replay_codes [tcl::dict::get $rinfo replay_codes]
- set rendered [tcl::dict::get $rinfo result]
- set overflow_right [tcl::dict::get $rinfo overflow_right]
- set unapplied [tcl::dict::get $rinfo unapplied]
- if {!$opt_overflow} {
- if {$opt_ellipsis} {
- set show_ellipsis 1
- if {!$opt_ellipsiswhitespace} {
- #we don't want ellipsis if only whitespace was lost
- #don't use tcl::string::range on ANSI data
- #set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end]
- set lostdata ""
- if {$overflow_right ne ""} {
- append lostdata $overflow_right
- }
- if {$unapplied ne ""} {
- append lostdata $unapplied
- }
- if {[tcl::string::trim $lostdata] eq ""} {
- set show_ellipsis 0
- }
- }
- if {$show_ellipsis} {
- set rendered [overtype::block -blockalign right $rendered $opt_ellipsistext]
- }
- }
-
- #if {$opt_ellipsis} {
- # set show_ellipsis 1
- # if {!$opt_ellipsiswhitespace} {
- # #we don't want ellipsis if only whitespace was lost
- # set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end]
- # if {[tcl::string::trim $lostdata] eq ""} {
- # set show_ellipsis 0
- # }
- # }
- # if {$show_ellipsis} {
- # set ellipsis [tcl::string::cat $replay_codes $opt_ellipsistext]
- # #todo - overflow on left if allign = right??
- # set rendered [overtype::right $rendered $ellipsis]
- # }
- #}
- }
- lappend outputlines $rendered
- } else {
- #padded overtext
- #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent -startcolumn [expr {$left_exposed + 1}] $undertext $overtext]
- #Note - we still need expand_right here - as although the overtext is short - it may oveflow due to the startoffset
- set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -expand_right $opt_overflow -startcolumn [expr {$left_exposed + 1 + $startoffset}] $undertext $overtext]
- #puts stderr "--> [ansistring VIEW -lf 1 -nul 1 $rinfo] <--"
- set overflow_right [tcl::dict::get $rinfo overflow_right]
- set unapplied [tcl::dict::get $rinfo unapplied]
- lappend outputlines [tcl::dict::get $rinfo result]
- }
- set replay_codes [tcl::dict::get $rinfo replay_codes]
- set replay_codes_underlay [tcl::dict::get $rinfo replay_codes_underlay]
- set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay]
- }
-
- return [join $outputlines \n]
- }
-
- variable optimise_ptruns 10 ;# can be set to zero to disable the ptruns branches
-
- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ###
- # renderline written from a left-right line orientation perspective as a first-shot at getting something useful.
- # ultimately right-to-left, top-to-bottom and bottom-to-top are probably needed.
- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ###
- #
- #
- #-returnextra enables returning of overflow and length
- #review - use punk::ansi::ta::detect to short-circuit processing and do simpler string calcs as an optimisation?
- #review - DECSWL/DECDWL double width line codes - very difficult/impossible to align and compose with other elements
- #(could render it by faking it with sixels and a lot of work - find/make a sixel font and ensure it's exactly 2 cols per char?
- # This would probably be impractical to support for different fonts)
- #todo - review transparency issues with single/double width characters
- #bidi - need a base direction and concept of directional runs for RTL vs LTR - may be best handled at another layer?
- proc renderline {args} {
- #*** !doctools
- #[call [fun overtype::renderline] [arg args] ]
- #[para] renderline is the core engine for overtype string processing (frames & textblocks), and the raw mode commandline repl for the Tcl Punk Shell
- #[para] It is also a central part of an ansi (micro) virtual terminal-emulator of sorts
- #[para] This system does a half decent job at rendering 90's ANSI art to manipulable colour text blocks that can be joined & framed for layout display within a unix or windows terminal
- #[para] Renderline helps maintain ANSI text styling reset/replay codes so that the styling of one block doesn't affect another.
- #[para] Calling on the punk::ansi library - it can coalesce codes to keep the size down.
- #[para] It is a giant mess of doing exactly what common wisdom says not to do... lots at once.
- #[para] renderline is part of the Unicode and ANSI aware Overtype system which 'renders' a block of text onto a static underlay
- #[para] The underlay is generally expected to be an ordered set of lines or a rectangular text block analogous to a terminal screen - but it can also be ragged in line length, or just blank.
- #[para] The overlay couuld be similar - in which case it may often be used to overwrite a column or section of the underlay.
- #[para] The overlay could however be a sequence of ANSI-laden text that jumps all over the place.
- #
- #[para] renderline itself only deals with a single line - or sometimes a single character. It is generally called from a loop that does further terminal-like or textblock processing.
- #[para] By suppyling the -info 1 option - it can return various fields indicating the state of the render.
- #[para] The main 3 are the result, overflow_right, and unapplied.
- #[para] Renderline handles cursor movements from either keystrokes or ANSI sequences but for a full system the aforementioned loop will need to be in place to manage the set of lines under manipulation.
-
- #puts stderr "renderline '$args'"
- variable optimise_ptruns
-
- if {[llength $args] < 2} {
- error {usage: ?-info 0|1? ?-startcolumn ? ?-cursor_column ? ?-cursor_row |""? ?-transparent [0|1|]? ?-expand_right [1|0]? undertext overtext}
- }
- set under [lindex $args end-1]
- set over [lindex $args end]
- #lassign [lrange $args end-1 end] under over
- if {[string last \n $under] >= 0} {
- error "overtype::renderline not allowed to contain newlines in undertext"
- }
- #if {[string first \n $over] >=0 || [string first \n $under] >= 0} {
- # error "overtype::renderline not allowed to contain newlines"
- #}
-
- #generally faster to create a new dict in the proc than to use a namespace variable to create dict once and link to variable (2024 8.6/8.7)
- set opts [tcl::dict::create\
- -etabs 0\
- -width \uFFEF\
- -expand_right 0\
- -transparent 0\
- -startcolumn 1\
- -cursor_column 1\
- -cursor_row ""\
- -insert_mode 1\
- -crm_mode 0\
- -autowrap_mode 1\
- -reverse_mode 0\
- -info 0\
- -exposed1 \uFFFD\
- -exposed2 \uFFFD\
- -cursor_restore_attributes ""\
- -cp437 0\
- -experimental {}\
- ]
- #-cursor_restore_attributes only - for replay stack - position and actual setting/restoring handled by throwback to caller
-
- #cursor_row, when numeric will allow detection of certain row moves that are still within our row - allowing us to avoid an early return
- #An empty string for cursor_row tells us we have no info about our own row context, and to return with an unapplied string if any row move occurs
-
- #exposed1 and exposed2 for first and second col of underying 2wide char which is truncated by transparency, currsor movements to 2nd charcol, or overflow/expand_right
- #todo - return info about such grapheme 'cuts' in -info structure and/or create option to raise an error
-
- set argsflags [lrange $args 0 end-2]
- tcl::dict::for {k v} $argsflags {
- switch -- $k {
- -experimental - -cp437 - -width - -expand_right - -transparent - -startcolumn - -cursor_column - -cursor_row
- - -crm_mode - -insert_mode - -autowrap_mode - -reverse_mode
- - -info - -exposed1 - -exposed2 - -cursor_restore_attributes {
- tcl::dict::set opts $k $v
- }
- default {
- error "overtype::renderline unknown option '$k'. Known options: [tcl::dict::keys $opts]"
- }
- }
- }
- # -- --- --- --- --- --- --- --- --- --- --- ---
- set opt_width [tcl::dict::get $opts -width]
- set opt_etabs [tcl::dict::get $opts -etabs]
- set opt_expand_right [tcl::dict::get $opts -expand_right]
- set opt_colstart [tcl::dict::get $opts -startcolumn] ;#lhs limit for overlay - an offset to cursor_column - first visible column is 1. 0 or < 0 are before the start of the underlay
- set opt_colcursor [tcl::dict::get $opts -cursor_column];#start cursor column relative to overlay
- set opt_row_context [tcl::dict::get $opts -cursor_row]
- if {[string length $opt_row_context]} {
- if {![tcl::string::is integer -strict $opt_row_context] || $opt_row_context <1 } {
- error "overtype::renderline -cursor_row must be empty for unspecified/unknown or a non-zero positive integer. received: '$opt_row_context'"
- }
- }
- # -- --- --- --- --- --- --- --- --- --- --- ---
- #The _mode flags correspond to terminal modes that can be set/reset via escape sequences (e.g DECAWM wraparound mode)
- set opt_insert_mode [tcl::dict::get $opts -insert_mode];#should usually be 1 for each new line in editor mode but must be initialised to 1 externally (review)
- #default is for overtype
- # -- --- --- --- --- --- --- --- --- --- --- ---
- set opt_autowrap_mode [tcl::dict::get $opts -autowrap_mode] ;#DECAWM - char or movement can go beyond leftmost/rightmost col to prev/next line
- set opt_reverse_mode [tcl::dict::get $opts -reverse_mode] ;#DECSNM
- set opt_crm_mode [tcl::dict::get $opts -crm_mode];# CRM - show control character mode
- # -- --- --- --- --- --- --- --- --- --- --- ---
- set temp_cursor_saved [tcl::dict::get $opts -cursor_restore_attributes]
-
- set cp437_glyphs [tcl::dict::get $opts -cp437]
- set cp437_map [tcl::dict::create]
- if {$cp437_glyphs} {
- set cp437_map [set ::punk::ansi::cp437_map]
- #for cp437 images we need to map these *after* splitting ansi
- #some old files might use newline for its glyph.. but we can't easily support that.
- #Not sure how old files did it.. maybe cr lf in sequence was newline and any lone cr or lf were displayed as glyphs?
- tcl::dict::unset cp437_map \n
- }
-
- set opt_transparent [tcl::dict::get $opts -transparent]
- if {$opt_transparent eq "0"} {
- set do_transparency 0
- } else {
- set do_transparency 1
- if {$opt_transparent eq "1"} {
- set opt_transparent {[\s]}
- }
- }
- # -- --- --- --- --- --- --- --- --- --- --- ---
- set opt_returnextra [tcl::dict::get $opts -info]
- # -- --- --- --- --- --- --- --- --- --- --- ---
- set opt_exposed1 [tcl::dict::get $opts -exposed1]
- set opt_exposed2 [tcl::dict::get $opts -exposed2]
- # -- --- --- --- --- --- --- --- --- --- --- ---
-
- if {$opt_row_context eq ""} {
- set cursor_row 1
- } else {
- set cursor_row $opt_row_context
- }
-
- set insert_mode $opt_insert_mode ;#default 1
- set autowrap_mode $opt_autowrap_mode ;#default 1
- set crm_mode $opt_crm_mode ;#default 0 (Show Control Character mode)
- set reverse_mode $opt_reverse_mode
-
- #-----
- #
- if {[info exists punk::console::tabwidth]} {
- #punk console is updated if punk::console::set_tabstop_width is used or rep is started/restarted
- #It is way too slow to test the current width by querying the terminal here - so it could conceivably get out of sync
- #todo - we also need to handle the new threaded repl where console config is in a different thread.
- # - also - concept of sub-regions being mini-consoles with their own settings - 'id' for console, or use in/out channels as id?
- set tw $::punk::console::tabwidth
- } else {
- set tw 8
- }
-
- set overdata $over
- if {!$cp437_glyphs} {
- #REVIEW! tabify will give different answers for an ANSI colourised string vs plain text
- if {!$opt_etabs} {
- if {[string first \t $under] >= 0} {
- #set under [textutil::tabify::untabify2 $under]
- set under [textutil::tabify::untabifyLine $under $tw]
- }
- if {[string first \t $over] >= 0} {
- #set overdata [textutil::tabify::untabify2 $over]
- set overdata [textutil::tabify::untabifyLine $over $tw]
- }
- }
- }
- #-------
-
- #ta_detect ansi and do simpler processing?
-
- #we repeat tests for grapheme width in different loops - rather than create another datastructure to store widths based on column,
- #we'll use the grapheme_width_cached function as a lookup table of all graphemes encountered - as there will often be repeats in different positions anyway.
-
- # -- --- --- --- --- --- --- ---
- if {$under ne ""} {
- if {[punk::ansi::ta::detect $under]} {
- set undermap [punk::ansi::ta::split_codes_single $under]
- } else {
- #single plaintext part
- set undermap [list $under]
- }
- } else {
- set undermap [list]
- }
- set understacks [list]
- set understacks_gx [list]
- set pm_list [list]
-
- set i_u -1 ;#underlay may legitimately be empty
- set undercols [list]
- set u_codestack [list]
- #u_gx_stack probably isn't really a stack - I don't know if g0 g1 can stack or not - for now we support only g0 anyway
- set u_gx_stack [list] ;#separate stack for g0 (g1 g2 g3?) graphics on and off (DEC special graphics)
- #set pt_underchars "" ;#for string_columns length calculation for expand_right 0 truncation
- set remainder [list] ;#for returnextra
- foreach {pt code} $undermap {
- #pt = plain text
- #append pt_underchars $pt
- if {$pt ne ""} {
- if {$cp437_glyphs} {
- set pt [tcl::string::map $cp437_map $pt]
- }
- set is_ptrun 0
- if {$optimise_ptruns && [tcl::string::length $pt] >= $optimise_ptruns} {
- set p1 [tcl::string::index $pt 0]
- set hex [format %x [scan $p1 %c]] ;#punk::char::char_hex
- set re [tcl::string::cat {^[} \\U$hex {]+$}]
- set is_ptrun [regexp $re $pt]
- }
- if {$is_ptrun} {
- #switch -- $p1 {
- # " " - - - _ - ! - @ - # - $ - % - ^ - & - * - = - + - : - . - , - / - | - ? -
- # a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y -
- # z - A - B - C - D - E - F - G - H - I - J - K - L - M - N - O - P - Q - R - S - T - U - V - W - X - Y - Z - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 {
- # set width 1
- # }
- # default {
- # if {$p1 eq "\u0000"} {
- # #use null as empty cell representation - review
- # #use of this will probably collide with some application at some point
- # #consider an option to set the empty cell character
- # set width 1
- # } else {
- # set width [grapheme_width_cached $p1] ;# when zero???
- # }
- # }
- #}
- set width [grapheme_width_cached $p1] ;# when zero???
- set ptlen [string length $pt]
- if {$width <= 1} {
- #review - 0 and 1?
- incr i_u $ptlen
- lappend understacks {*}[lrepeat $ptlen $u_codestack]
- lappend understacks_gx {*}[lrepeat $ptlen $u_gx_stack]
- lappend undercols {*}[lrepeat $ptlen $p1]
- } else {
- incr i_u $ptlen ;#2nd col empty str - so same as above
- set 2ptlen [expr {$ptlen * 2}]
- lappend understacks {*}[lrepeat $2ptlen $u_codestack]
- lappend understacks_gx {*}[lrepeat $2ptlen $u_gx_stack]
- set l [concat {*}[lrepeat $ptlen [list $p1 ""]]
- lappend undercols {*}$l
- unset l
- }
-
- } else {
- foreach grapheme [punk::char::grapheme_split $pt] {
- #an ugly but easy hack to serve *some* common case ascii quickly with byte-compiled literal switch - feels dirty.
- #.. but even 0.5uS per char (grapheme_width_cached) adds up quickly when stitching lots of lines together.
- #todo - test decimal value instead, compare performance
- switch -- $grapheme {
- " " - - - _ - ! - @ - # - $ - % - ^ - & - * - = - + - : - . - , - / - | - ? -
- a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y -
- z - A - B - C - D - E - F - G - H - I - J - K - L - M - N - O - P - Q - R - S - T - U - V - W - X - Y - Z - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 {
- set width 1
- }
- default {
- if {$grapheme eq "\u0000"} {
- #use null as empty cell representation - review
- #use of this will probably collide with some application at some point
- #consider an option to set the empty cell character
- set width 1
- } else {
- #zero width still acts as 1 below??? review what should happen
- set width [grapheme_width_cached $grapheme]
- #we still want most controls and other zero-length codepoints such as \u200d (zero width joiner) to stay zero-length
- #we substitute lone ESC that weren't captured within ANSI context as a debugging aid to see malformed ANSI
- #todo - default to off and add a flag (?) to enable this substitution
- set sub_stray_escapes 0
- if {$sub_stray_escapes && $width == 0} {
- if {$grapheme eq "\x1b"} {
- set gvis [ansistring VIEW $grapheme] ;#can only use with graphemes that have a single replacement char..
- set grapheme $gvis
- set width 1
- }
- }
- }
- }
- }
-
- #set width [grapheme_width_cached $grapheme]
- incr i_u
- lappend understacks $u_codestack
- lappend understacks_gx $u_gx_stack
-
- lappend undercols $grapheme
- if {$width > 1} {
- #presumably there are no triple-column or wider unicode chars.. until the aliens arrive.(?)
- #but what about emoji combinations etc - can they be wider than 2?
- #todo - if -etabs enabled - then we treat \t as the width determined by our elastic tabstop
- incr i_u
- lappend understacks $u_codestack
- lappend understacks_gx $u_gx_stack
- lappend undercols ""
- }
- }
-
- }
- }
- #underlay should already have been rendered and not have non-sgr codes - but let's retain the check for them and not stack them if other codes are here
-
- #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc
- #keep any remaining PMs in place
- if {$code ne ""} {
- set c1c2 [tcl::string::range $code 0 1]
-
- set leadernorm [tcl::string::range [tcl::string::map [list\
- \x1b\[ 7CSI\
- \x9b 8CSI\
- \x1b\( 7GFX\
- \x1b^ 7PMX\
- \x1bX 7SOS\
- ] $c1c2] 0 3];# leadernorm is 1st 2 chars mapped to normalised indicator - or is original 2 chars
-
- switch -- $leadernorm {
- 7CSI - 8CSI {
- #need to exclude certain leaders after the lb e.g < for SGR 1006 mouse
- #REVIEW - what else could end in m but be mistaken as a normal SGR code here?
- set maybemouse ""
- if {[tcl::string::index $c1c2 0] eq "\x1b"} {
- set maybemouse [tcl::string::index $code 2]
- }
-
- if {$maybemouse ne "<" && [tcl::string::index $code end] eq "m"} {
- if {[punk::ansi::codetype::is_sgr_reset $code]} {
- set u_codestack [list "\x1b\[m"]
- } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} {
- set u_codestack [list $code]
- } else {
- #basic simplification first.. straight dups
- set dup_posns [lsearch -all -exact $u_codestack $code] ;#-exact because of square-bracket glob chars
- set u_codestack [lremove $u_codestack {*}$dup_posns]
- lappend u_codestack $code
- }
- }
- }
- 7GFX {
- switch -- [tcl::string::index $code 2] {
- "0" {
- set u_gx_stack [list gx0_on] ;#we'd better use a placeholder - or debugging will probably get into a big mess
- }
- B {
- set u_gx_stack [list]
- }
- }
- }
- 7PMX - 7SOS {
- #we can have PMs or SOS (start of string) in the underlay - though mostly the PMs should have been processed..
- #attach the PM/SOS (entire ANSI sequence) to the previous grapheme!
- #It should not affect the size - but terminal display may get thrown out if terminal doesn't support them.
-
- #note that there may in theory already be ANSI stored - we don't assume it was a pure grapheme string
- set graphemeplus [lindex $undercols end]
- if {$graphemeplus ne "\0"} {
- append graphemeplus $code
- } else {
- set graphemeplus $code
- }
- lset undercols end $graphemeplus
- #The grapheme_width_cached function will be called on this later - and doesn't account for ansi.
- #we need to manually cache the item with it's proper width
- variable grapheme_widths
- #stripped and plus version keys pointing to same length
- dict set grapheme_widths $graphemeplus [grapheme_width_cached [::punk::ansi::ansistrip $graphemeplus]]
-
- }
- default {
-
- }
-
- }
-
- #if {[punk::ansi::codetype::is_sgr_reset $code]} {
- # #set u_codestack [list]
- #} elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} {
- #} elseif {[punk::ansi::codetype::is_sgr $code]} {
- #} else {
- # #leave SGR stack as is
- # if {[punk::ansi::codetype::is_gx_open $code]} {
- # } elseif {[punk::ansi::codetype::is_gx_close $code]} {
- # }
- #}
- }
- #consider also if there are other codes that should be stacked..?
- }
-
- #NULL empty cell indicator
- if {$opt_width ne "\uFFEF"} {
- if {[llength $understacks]} {
- set cs $u_codestack
- set gs $u_gx_stack
- } else {
- set cs [list]
- set gs [list]
- }
- if {[llength $undercols]< $opt_width} {
- set diff [expr {$opt_width- [llength $undercols]}]
- if {$diff > 0} {
- #set undercols [list {*}$undercols {*}[lrepeat $diff "\u0000"]] ;#2024 - much slower
- lappend undercols {*}[lrepeat $diff "\u0000"]
- lappend understacks {*}[lrepeat $diff $cs]
- lappend understacks_gx {*}[lrepeat $diff $gs]
- }
- }
- }
-
- if {$opt_width ne "\uFFEF"} {
- set renderwidth $opt_width
- } else {
- set renderwidth [llength $undercols]
- }
-
-
- if 0 {
- # -----------------
- # if we aren't extending understacks & understacks_gx each time we incr idx above the undercols length.. this doesn't really serve a purpose
- # Review.
- # -----------------
- #replay code for last overlay position in input line
- # whether or not we get that far - we need to return it for possible replay on next line
- if {[llength $understacks]} {
- lappend understacks $u_codestack
- lappend understacks_gx $u_gx_stack
- } else {
- #in case overlay onto emptystring as underlay
- lappend understacks [list]
- lappend understacks_gx [list]
- }
- # -----------------
- }
-
- #trailing codes in effect for underlay
- if {[llength $u_codestack]} {
- #set replay_codes_underlay [join $u_codestack ""]
- set replay_codes_underlay [punk::ansi::codetype::sgr_merge_list {*}$u_codestack]
- } else {
- set replay_codes_underlay ""
- }
-
-
- # -- --- --- --- --- --- --- ---
- ####
- #if opt_colstart - we need to build a space (or any singlewidth char ?) padding on the left containing the right number of columns.
- #this will be processed as transparent - and handle doublewidth underlay characters appropriately
- set startpadding [string repeat " " [expr {$opt_colstart -1}]]
- #overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency
- if {$startpadding ne "" || $overdata ne ""} {
- if {[punk::ansi::ta::detect $overdata]} {
- set overmap [punk::ansi::ta::split_codes_single $startpadding$overdata]
- } else {
- #single plaintext part
- set overmap [list $startpadding$overdata]
- }
- } else {
- set overmap [list]
- }
- ####
-
-
- #todo - detect plain ascii no-ansi input line (aside from leading ansi reset)
- #will that allow some optimisations?
-
- #todo - detect repeated transparent char in overlay
- #regexp {^(.)\1+$} ;#backtracking regexp - relatively slow.
- # - try set c [string index $data 0]; regexp [string map [list %c% $c] {^[%c%]+$}] $data
- #we should be able to optimize to pass through the underlay??
-
- #???
- set colcursor $opt_colstart
- #TODO - make a little virtual column object
- #we need to refer to column1 or columnmin? or columnmax without calculating offsets due to to startcolumn
- #need to lock-down what start column means from perspective of ANSI codes moving around - the offset perspective is unclear and a mess.
-
-
- #set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+}
- #as at 2024-02 punk::char::grapheme_split uses these - not aware of more complex graphemes
-
- set overstacks [list]
- set overstacks_gx [list]
-
- set o_codestack [list]; #SGR codestack (not other codes such as movement,insert key etc)
- set o_gxstack [list]
- set pt_overchars ""
- set i_o 0
- set overlay_grapheme_control_list [list] ;#tag each with g, sgr or other. 'other' are things like cursor-movement or insert-mode or codes we don't recognise/use
- #experiment
- set overlay_grapheme_control_stacks [list]
- foreach {pt code} $overmap {
- if {$pt ne ""} {
- #todo - wrap in test for empty pt (we used split_codes_single - and it may be common for sgr sequences to be unmerged and so have empty pts between)
- if {$cp437_glyphs} {
- set pt [tcl::string::map $cp437_map $pt]
- }
- append pt_overchars $pt
- #will get empty pt between adjacent codes
- if {!$crm_mode} {
-
- set is_ptrun 0
- if {$optimise_ptruns && [tcl::string::length $pt] >= $optimise_ptruns} {
- set p1 [tcl::string::index $pt 0]
- set re [tcl::string::cat {^[} \\U[format %x [scan $p1 %c]] {]+$}]
- set is_ptrun [regexp $re $pt]
-
- #leading only? we would have to check for graphemes at the trailing boundary?
- #set re [tcl::string::cat {^[} \\U[format %x [scan $p1 %c]] {]+}]
- #set is_ptrun [regexp -indices $re $pt runrange]
- #if {$is_ptrun && 1} {
- #}
- }
- if {$is_ptrun} {
- #review - in theory a run over a certain length won't be part of some grapheme combo (graphemes can be long e.g 44?, but not as runs(?))
- #could be edge cases for runs at line end? (should be ok as we include trailing \n in our data)
- set len [string length $pt]
- set g_element [list g $p1]
-
- #lappend overstacks {*}[lrepeat $len $o_codestack]
- #lappend overstacks_gx {*}[lrepeat $len $o_gxstack]
- #incr i_o $len
- #lappend overlay_grapheme_control_list {*}[lrepeat $len [list g $p1]]
- #lappend overlay_grapheme_control_stacks {*}[lrepeat $len $o_codestack]
-
- set pi 0
- incr i_o $len
- while {$pi < $len} {
- lappend overstacks $o_codestack
- lappend overstacks_gx $o_gxstack
- lappend overlay_grapheme_control_list $g_element
- lappend overlay_grapheme_control_stacks $o_codestack
- incr pi
- }
- } else {
- foreach grapheme [punk::char::grapheme_split $pt] {
- lappend overstacks $o_codestack
- lappend overstacks_gx $o_gxstack
- incr i_o
- lappend overlay_grapheme_control_list [list g $grapheme]
- lappend overlay_grapheme_control_stacks $o_codestack
- }
- }
- } else {
- set tsbegin [clock micros]
- foreach grapheme_original [punk::char::grapheme_split $pt] {
- set pt_crm [ansistring VIEW -nul 1 -lf 2 -vt 2 -ff 2 $grapheme_original]
- #puts stderr "ptlen [string length $pt] graphemelen[string length $grapheme_original] pt_crmlen[string length $pt_crm] $pt_crm"
- foreach grapheme [punk::char::grapheme_split $pt_crm] {
- if {$grapheme eq "\n"} {
- lappend overlay_grapheme_control_stacks $o_codestack
- lappend overlay_grapheme_control_list [list crmcontrol "\x1b\[00001E"]
- } else {
- lappend overstacks $o_codestack
- lappend overstacks_gx $o_gxstack
- incr i_o
- lappend overlay_grapheme_control_list [list g $grapheme]
- lappend overlay_grapheme_control_stacks $o_codestack
- }
- }
- }
- set elapsed [expr {[clock micros] - $tsbegin}]
- puts stderr "ptlen [string length $pt] elapsedus:$elapsed"
- }
- }
-
- #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc
- #order of if-else based on assumptions:
- # that pure resets are fairly common - more so than leading resets with other info
- # that non-sgr codes are not that common, so ok to check for resets before verifying it is actually SGR at all.
- if {$code ne ""} {
- #we need to immediately set crm_mode here if \x1b\[3h received
- if {$code eq "\x1b\[3h"} {
- set crm_mode 1
- } elseif {$code eq "\x1b\[3l"} {
- set crm_mode 0
- }
- #else crm_mode could be set either way from options
- if {$crm_mode && $code ne "\x1b\[00001E"} {
- #treat the code as type 'g' like above - only allow through codes to reset mode REVIEW for now just \x1b\[3l ?
- #we need to somehow convert further \n in the graphical rep to an instruction for newline that will bypass further crm_mode processing or we would loop.
- set code_as_pt [ansistring VIEW -nul 1 -lf 1 -vt 1 -ff 1 $code]
- #split using standard split for first foreach loop - grapheme based split when processing 2nd foreach loop
- set chars [split $code_as_pt ""]
- set codeparts [list] ;#list of 2-el lists each element {crmcontrol } or {g }
- foreach c $chars {
- if {$c eq "\n"} {
- #use CNL (cursor next line) \x1b\[00001E ;#leading zeroes ok for this processor - used as debugging aid to distinguish
- lappend codeparts [list crmcontrol "\x1b\[00001E"]
- } else {
- if {[llength $codeparts] > 0 && [lindex $codeparts end 0] eq "g"} {
- set existing [lindex $codeparts end 1]
- lset codeparts end [list g [string cat $existing $c]]
- } else {
- lappend codeparts [list g $c]
- }
- }
- }
-
- set partidx 0
- foreach record $codeparts {
- lassign $record rtype rval
- switch -exact -- $rtype {
- g {
- append pt_overchars $rval
- foreach grapheme [punk::char::grapheme_split $rval] {
- lappend overstacks $o_codestack
- lappend overstacks_gx $o_gxstack
- incr i_o
- lappend overlay_grapheme_control_list [list g $grapheme]
- lappend overlay_grapheme_control_stacks $o_codestack
- }
- }
- crmcontrol {
- #leave o_codestack
- lappend overlay_grapheme_control_stacks $o_codestack
- lappend overlay_grapheme_control_list [list crmcontrol $rval]
- }
- }
- }
- } else {
- lappend overlay_grapheme_control_stacks $o_codestack
- #there will always be an empty code at end due to foreach on 2 vars with odd-sized list ending with pt (overmap coming from perlish split)
- if {[punk::ansi::codetype::is_sgr_reset $code]} {
- set o_codestack [list "\x1b\[m"] ;#reset better than empty list - fixes some ansi art issues
- lappend overlay_grapheme_control_list [list sgr $code]
- } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} {
- set o_codestack [list $code]
- lappend overlay_grapheme_control_list [list sgr $code]
- } elseif {[priv::is_sgr $code]} {
- #basic simplification first - remove straight dupes
- set dup_posns [lsearch -all -exact $o_codestack $code] ;#must be -exact because of square-bracket glob chars
- set o_codestack [lremove $o_codestack {*}$dup_posns]
- lappend o_codestack $code
- lappend overlay_grapheme_control_list [list sgr $code]
- } elseif {[regexp {\x1b7|\x1b\[s} $code]} {
- #experiment
- #cursor_save - for the replays review.
- #jmn
- #set temp_cursor_saved [punk::ansi::codetype::sgr_merge_list {*}$o_codestack]
- lappend overlay_grapheme_control_list [list other $code]
- } elseif {[regexp {\x1b8|\x1b\[u} $code]} {
- #experiment
- #cursor_restore - for the replays
- set o_codestack [list $temp_cursor_saved]
- lappend overlay_grapheme_control_list [list other $code]
- } else {
- #review
- if {[punk::ansi::codetype::is_gx_open $code]} {
- set o_gxstack [list "gx0_on"]
- lappend overlay_grapheme_control_list [list gx0 gx0_on] ;#don't store code - will complicate debugging if we spit it out and jump character sets
- } elseif {[punk::ansi::codetype::is_gx_close $code]} {
- set o_gxstack [list]
- lappend overlay_grapheme_control_list [list gx0 gx0_off] ;#don't store code - will complicate debugging if we spit it out and jump character sets
- } else {
- lappend overlay_grapheme_control_list [list other $code]
- }
- }
- }
- }
-
- }
- #replay code for last overlay position in input line - should take account of possible trailing sgr code after last grapheme
- set max_overlay_grapheme_index [expr {$i_o -1}]
- lappend overstacks $o_codestack
- lappend overstacks_gx $o_gxstack
-
- #set replay_codes_overlay [join $o_codestack ""]
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}$o_codestack]
-
- #if {[tcl::dict::exists $overstacks $max_overlay_grapheme_index]} {
- # set replay_codes_overlay [join [tcl::dict::get $overstacks $max_overlay_grapheme_index] ""]
- #} else {
- # set replay_codes_overlay ""
- #}
- # -- --- --- --- --- --- --- ---
-
-
- #potential problem - combinining diacritics directly following control chars like \r \b
-
- # -- --- ---
- #we need to initialise overflow_idx before any potential row-movements - as they need to perform a loop break and force in_excess to 1
- if {$opt_expand_right} {
- #expand_right true means we can have lines as long as we want, but either way there can be excess data that needs to be thrown back to the calling loop.
- #we currently only support horizontal expansion to the right (review regarding RTL text!)
- set overflow_idx -1
- } else {
- #expand_right zero - we can't grow beyond our column width - so we get ellipsis or truncation
- if {$opt_width ne "\uFFEF"} {
- set overflow_idx [expr {$opt_width}]
- } else {
- #review - this is also the cursor position when adding a char at end of line?
- set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it
- }
- }
- # -- --- ---
-
- set outcols $undercols ;#leave undercols as is, outcols can potentially be appended to.
-
- set unapplied "" ;#if we break for move row (but not for /v ?)
- set unapplied_list [list]
-
- set insert_lines_above 0 ;#return key
- set insert_lines_below 0
- set instruction ""
-
- # -- --- ---
- #cursor_save_dec, cursor_restore_dec etc
- set cursor_restore_required 0
- set cursor_saved_attributes ""
- set cursor_saved_position ""
- # -- --- ---
-
- #set idx 0 ;# line index (cursor - 1)
- #set idx [expr {$opt_colstart + $opt_colcursor} -1]
-
- #idx is the per column output index
- set idx [expr {$opt_colcursor -1}] ;#don't use opt_colstart here - we have padded and won't start emitting until idx reaches opt_colstart-1
- #cursor_column is usually one above idx - but we have opt_colstart which is like a margin - todo: remove cursor_column from the following loop and calculate it's offset when breaking or at end.
- #(for now we are incrementing/decrementing both in sync - which is a bit silly)
- set cursor_column $opt_colcursor
-
- #idx_over is the per grapheme overlay index
- set idx_over -1
-
-
- #movements only occur within the overlay range.
- #an underlay is however not necessary.. e.g
- #renderline -expand_right 1 "" data
-
- #set re_mode {\x1b\[\?([0-9]*)(h|l)} ;#e.g DECAWM
- #set re_col_move {\x1b\[([0-9]*)(C|D|G)$}
- #set re_row_move {\x1b\[([0-9]*)(A|B)$}
- #set re_both_move {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)H$} ;# or "f" ?
- #set re_vt_sequence {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)~$}
- #set re_cursor_save {\x1b\[s$} ;#note probable incompatibility with DECSLRM (set left right margin)!
- #set re_cursor_restore {\x1b\[u$}
- #set re_cursor_save_dec {\x1b7$}
- #set re_cursor_restore_dec {\x1b8$}
- #set re_other_single {\x1b(D|M|E)$}
- #set re_decstbm {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)r$} ;#DECSTBM set top and bottom margins
-
- #puts "-->$overlay_grapheme_control_list<--"
- #puts "-->overflow_idx: $overflow_idx"
- for {set gci 0} {$gci < [llength $overlay_grapheme_control_list]} {incr gci} {
- set gc [lindex $overlay_grapheme_control_list $gci]
- lassign $gc type item
-
- #emit plaintext chars first using existing SGR codes from under/over stack as appropriate
- #then check if the following code is a cursor movement within the line and adjust index if so
- #foreach ch $overlay_graphemes {}
- switch -- $type {
- g {
- set ch $item
- #crm_mode affects both graphic and control
- if {0 && $crm_mode} {
- set chars [ansistring VIEW -nul 1 -lf 2 -vt 2 -ff 2 $ch]
- set chars [string map [list \n "\x1b\[00001E"] $chars]
- if {[llength [split $chars ""]] > 1} {
- priv::render_unapplied $overlay_grapheme_control_list $gci
- #prefix the unapplied controls with the string version of this control
- set unapplied_list [linsert $unapplied_list 0 {*}[split $chars ""]]
- set unapplied [join $unapplied_list ""]
- #incr idx_over
- break
- } else {
- set ch $chars
- }
- }
- incr idx_over; #idx_over (until unapplied reached anyway) is per *grapheme* in the overlay - not per col.
- if {($idx < ($opt_colstart -1))} {
- incr idx [grapheme_width_cached $ch]
- continue
- }
- #set within_undercols [expr {$idx <= [llength $undercols]-1}] ;#within our active data width
- set within_undercols [expr {$idx <= $renderwidth-1}]
-
- #https://www.enigma.com/resources/blog/the-secret-world-of-newline-characters
- #\x85 NEL in the c1 control set is treated by some terminal emulators (e.g Hyper) as a newline,
- #on some it's invisble but doesn't change the line, on some it's a visible glyph of width 1.
- #This is hard to process in any standard manner - but I think the Hyper behaviour of doing what it was intended is perhaps most reasonable
- #We will map it to the same behaviour as lf here for now... but we need also to consider the equivalent ANSI sequence: \x1bE
-
- set chtest [tcl::string::map [list \n \x85 \b \r \v \x7f ] $ch]
- #puts --->chtest:$chtest
- #specials - each shoud have it's own test of what to do if it happens after overflow_idx reached
- switch -- $chtest {
- "" {
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- if {$idx == 0} {
- #puts "---a at col 1"
- #linefeed at column 1
- #leave the overflow_idx ;#? review
- set instruction lf_start ;#specific instruction for newline at column 1
- priv::render_unapplied $overlay_grapheme_control_list $gci
- break
- } elseif {$overflow_idx != -1 && $idx == $overflow_idx} {
- #linefeed after final column
- #puts "---c at overflow_idx=$overflow_idx"
- incr cursor_row
- set overflow_idx $idx ;#override overflow_idx even if it was set to -1 due to expand_right = 1
- set instruction lf_overflow ;#only special treatment is to give it it's own instruction in case caller needs to handle differently
- priv::render_unapplied $overlay_grapheme_control_list $gci
- break
- } else {
- #linefeed occurred in middle or at end of text
- #puts "---mid-or-end-text-linefeed idx:$idx overflow_idx:$overflow_idx"
- if {$insert_mode == 0} {
- incr cursor_row
- if {$idx == -1 || $overflow_idx > $idx} {
- #don't set overflow_idx higher if it's already set lower and we're adding graphemes to overflow
- set overflow_idx $idx ;#override overflow_idx even if it was set to -1 due to expand_right = 1
- }
- set instruction lf_mid
- priv::render_unapplied $overlay_grapheme_control_list $gci
- break
- } else {
- incr cursor_row
- #don't adjust the overflow_idx
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction lf_mid
- break ;# could have overdata following the \n - don't keep processing
- }
- }
-
- }
- "" {
- #will we want/need to use raw for keypresses in terminal? (terminal with LNM in standard reset mode means enter= this is the usual config for terminals)
- #So far we are assuming the caller has translated to and handle above.. REVIEW.
-
- #consider also the old space-carriagereturn softwrap convention used in some terminals.
- #In the context of rendering to a block of text - this works similarly in that the space gets eaten so programs emitting space-cr at the terminal width col will pretty much get what they expect.
- set idx [expr {$opt_colstart -1}]
- set cursor_column $opt_colstart ;#?
- }
- "" {
- #literal backspace char - not necessarily from keyboard
- #review - backspace effect on double-width chars - we are taking a column-editing perspective in overtype
- #(important for -transparent option - hence replacement chars for half-exposed etc)
- #review - overstrike support as per nroff/less (generally considered an old technology replaced by unicode mechanisms and/or ansi SGR)
- if {$idx > ($opt_colstart -1)} {
- incr idx -1
- incr cursor_column -1
- } else {
- set flag 0
- if $flag {
- #review - conflicting requirements? Need a different sequence for destructive interactive backspace?
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction backspace_at_start
- break
- }
- }
- }
- "" {
- #literal del character - some terminals send just this for what is generally expected to be a destructive backspace
- #We instead treat this as a pure delete at current cursor position - it is up to the repl or terminal to remap backspace key to a sequence that has the desired effect.
- priv::render_delchar $idx
- }
- "" {
- #end processing this overline. rest of line is remainder. cursor for column as is.
- #REVIEW - this theoretically depends on terminal's vertical tabulation setting (name?)
- #e.g it could be configured to jump down 6 rows.
- #On the other hand I've seen indications that some modern terminal emulators treat it pretty much as a linefeed.
- #todo?
- incr cursor_row
- set overflow_idx $idx
- #idx_over has already been incremented as this is both a movement-control and in some sense a grapheme
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction vt
- break
- }
- default {
- if {$overflow_idx != -1} {
- #review - how to check arbitrary length item such as tab is going to overflow .. before we get to overflow_idx?
- #call grapheme_width_cached on each ch, or look for tab specifically as it's currently the only known reason to have a grapheme width > 2?
- #we need to decide what a tab spanning the overflow_idx means and how it affects wrap etc etc
- if {$idx == $overflow_idx-1} {
- set owidth [grapheme_width_cached $ch]
- if {$owidth == 2} {
- #review split 2w overflow?
- #we don't want to make the decision here to split a 2w into replacement characters at end of line and beginning of next line
- #better to consider the overlay char as unable to be applied to the line
- #render empty column(?) - and reduce overlay grapheme index by one so that the current ch goes into unapplied
- #throwing back to caller with instruction complicates its job - but is necessary to avoid making decsions for it here.
- priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- #change the overflow_idx
- set overflow_idx $idx
- incr idx
- incr idx_over -1 ;#set overlay grapheme index back one so that sgr stack from previous overlay grapheme used
- priv::render_unapplied $overlay_grapheme_control_list [expr {$gci-1}] ;#note $gci-1 instead of just gci
- #throw back to caller's loop - add instruction to caller as this is not the usual case
- #caller may for example choose to render a single replacement char to this line and omit the grapheme, or wrap it to the next line
- set instruction overflow_splitchar
- break
- } elseif {$owidth > 2} {
- #? tab?
- #TODO!
- puts stderr "overtype::renderline long overtext grapheme '[ansistring VIEW -lf 1 -vt 1 $ch]' not handled"
- #tab of some length dependent on tabstops/elastic tabstop settings?
- }
- } elseif {$idx >= $overflow_idx} {
- #REVIEW
- set next_gc [lindex $overlay_grapheme_control_list $gci+1] ;#next grapheme or control
- lassign $next_gc next_type next_item
- if {$autowrap_mode || $next_type ne "g"} {
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci-1]]
- #set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- #don't incr idx beyond the overflow_idx
- #idx_over already incremented - decrement so current overlay grapheme stacks go to unapplied
- incr idx_over -1
- #priv::render_unapplied $overlay_grapheme_control_list [expr {$gci-1}] ;#back one index here too
- priv::render_this_unapplied $overlay_grapheme_control_list $gci ;#
- set instruction overflow
- break
- } else {
- #no point throwing back to caller for each grapheme that is overflowing
- #without this branch - renderline would be called with overtext reducing only by one grapheme per call
- #processing a potentially long overtext each time (ie - very slow)
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- #JMN4
-
- }
- }
- } else {
- #review.
- #overflow_idx = -1
- #This corresponds to expand_right being true (at least until overflow_idx is in some cases forced to a value when throwing back to calling loop)
- }
-
- if {($do_transparency && [regexp $opt_transparent $ch])} {
- #pre opt_colstart is effectively transparent (we have applied padding of required number of columns to left of overlay)
- if {$idx > [llength $outcols]-1} {
- lappend outcols " "
- #tcl::dict::set understacks $idx [list] ;#review - use idx-1 codestack?
- #lset understacks $idx [list] ;#will get index $i out of range error
- lappend understacks [list] ;#REVIEW
- incr idx
- incr cursor_column
- } else {
- #todo - punk::char::char_width
- set g [lindex $outcols $idx]
- #JMN
- set uwidth [grapheme_width_cached $g]
- if {[lindex $outcols $idx] eq ""} {
- #2nd col of 2-wide char in underlay
- incr idx
- incr cursor_column
- } elseif {$uwidth == 0} {
- #e.g control char ? combining diacritic ?
- incr idx
- incr cursor_column
- } elseif {$uwidth == 1} {
- set owidth [grapheme_width_cached $ch]
- incr idx
- incr cursor_column
- if {$owidth > 1} {
- incr idx
- incr cursor_column
- }
- } elseif {$uwidth > 1} {
- if {[grapheme_width_cached $ch] == 1} {
- if {!$insert_mode} {
- #normal singlewide transparent overlay onto double-wide underlay
- set next_pt_overchar [tcl::string::index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay
- if {$next_pt_overchar eq ""} {
- #special-case trailing transparent - no next_pt_overchar
- incr idx
- incr cursor_column
- } else {
- if {[regexp $opt_transparent $next_pt_overchar]} {
- incr idx
- incr cursor_column
- } else {
- #next overlay char is not transparent.. first-half of underlying 2wide char is exposed
- #priv::render_addchar $idx $opt_exposed1 [tcl::dict::get $overstacks $idx_over] [tcl::dict::get $overstacks_gx $idx_over] $insert_mode
- priv::render_addchar $idx $opt_exposed1 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- incr idx
- incr cursor_column
- }
- }
- } else {
- #? todo - decide what transparency even means for insert mode
- incr idx
- incr cursor_column
- }
- } else {
- #2wide transparency over 2wide in underlay - review
- incr idx
- incr cursor_column
- }
- }
- }
- } else {
-
- set idxchar [lindex $outcols $idx]
- #non-transparent char in overlay or empty cell
- if {$idxchar eq "\u0000"} {
- #empty/erased cell indicator
- set uwidth 1
- } else {
- set uwidth [grapheme_width_cached $idxchar]
- }
- if {$within_undercols} {
- if {$idxchar eq ""} {
- #2nd col of 2wide char in underlay
- if {!$insert_mode} {
- priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 0
- #JMN - this has to expose if our startposn chopped an underlay - but not if we already overwrote the first half of the widechar underlay grapheme
- #e.g renderline \uFF21\uFF21--- a\uFF23\uFF23
- #vs
- # renderline -startcolumn 2 \uFF21---- \uFF23
- if {[lindex $outcols $idx-1] != ""} {
- #verified it's an empty following a filled - so it's a legit underlay remnant (REVIEW - when would it not be??)
- #reset previous to an exposed 1st-half - but leave understacks code as is
- priv::render_addchar [expr {$idx-1}] $opt_exposed1 [lindex $understacks $idx-1] [lindex $understacks_gx $idx-1] 0
- }
- incr idx
- } else {
- set prevcolinfo [lindex $outcols $idx-1]
- #for insert mode - first replace the empty 2ndhalf char with exposed2 before shifting it right
- #REVIEW - this leaves a replacement character permanently in our columns.. but it is consistent regarding length (?)
- #The alternative is to disallow insertion at a column cursor that is at 2nd half of 2wide char
- #perhaps by inserting after the char - this may be worthwhile - but may cause other surprises
- #It is perhaps best avoided at another level and try to make renderline do exactly as it's told
- #the advantage of this 2w splitting method is that the inserted character ends up in exactly the column we expect.
- priv::render_addchar $idx $opt_exposed2 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 0 ;#replace not insert
- priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 1 ;#insert - same index
- if {$prevcolinfo ne ""} {
- #we've split the 2wide - it may already have been rendered as an exposed1 - but not for example if our startcolumn was current idx
- priv::render_addchar [expr {$idx-1}] $opt_exposed1 [lindex $understacks $idx-1] [lindex $understacks_gx $idx-1] 0 ;#replace not insert
- } ;# else??
- incr idx
- }
- if {$cursor_column < [llength $outcols] || $overflow_idx == -1} {
- incr cursor_column
- }
- } elseif {$uwidth == 0} {
- #what if this is some other c0/c1 control we haven't handled specifically?
-
- #by emitting a preceding empty-string column - we associate whatever this char is with the preceeding non-zero-length character and any existing zero-lengths that follow it
- #e.g combining diacritic - increment before over char REVIEW
- #arguably the previous overchar should have done this - ie lookahead for combiners?
- #if we can get a proper grapheme_split function - this should be easier to tidy up.
- priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- incr idx
- priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- incr idx
- incr cursor_column 2
-
- if {$cursor_column > [llength $outcols] && $overflow_idx != -1} {
- set cursor_column [llength $outcols]
- }
- } elseif {$uwidth == 1} {
- #includes null empty cells
- set owidth [grapheme_width_cached $ch]
- if {$owidth == 1} {
- priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- incr idx
- } else {
- priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- incr idx
- priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- #if next column in underlay empty - we've overwritten first half of underlying 2wide grapheme
- #replace with rhs exposure in case there are no more overlay graphemes coming - use underlay's stack
- if {([llength $outcols] >= $idx +2) && [lindex $outcols $idx+1] eq ""} {
- priv::render_addchar [expr {$idx+1}] $opt_exposed2 [lindex $understacks $idx+1] [lindex $understacks_gx $idx+1] $insert_mode
- }
- incr idx
- }
- if {($cursor_column < [llength $outcols]) || $overflow_idx == -1} {
- incr cursor_column
- }
- } elseif {$uwidth > 1} {
- set owidth [grapheme_width_cached $ch]
- if {$owidth == 1} {
- #1wide over 2wide in underlay
- if {!$insert_mode} {
- priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- incr idx
- incr cursor_column
- priv::render_addchar $idx $opt_exposed2 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- #don't incr idx - we are just putting a broken-indication in the underlay - which may get overwritten by next overlay char
- } else {
- #insert mode just pushes all to right - no exposition char here
- priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- incr idx
- incr cursor_column
- }
- } else {
- #2wide over 2wide
- priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- incr idx 2
- incr cursor_column 2
- }
-
- if {$cursor_column > [llength $outcols] && $overflow_idx != -1} {
- set cursor_column [llength $outcols]
- }
- }
- } else {
- priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- incr idx
- incr cursor_column
- }
- }
- }
- } ;# end switch
-
-
- }
- other - crmcontrol {
- if {$crm_mode && $type ne "crmcontrol" && $item ne "\x1b\[00001E"} {
- if {$item eq "\x1b\[3l"} {
- set crm_mode 0
- } else {
- #When our initial overlay split was done - we weren't in crm_mode - so there are codes that weren't mapped to unicode control character representations
- #set within_undercols [expr {$idx <= $renderwidth-1}]
- #set chars [ansistring VIEW -nul 1 -lf 2 -vt 2 -ff 2 $item]
- set chars [ansistring VIEW -nul 1 -lf 1 -vt 1 -ff 1 $item]
- priv::render_unapplied $overlay_grapheme_control_list $gci
- #prefix the unapplied controls with the string version of this control
- set unapplied_list [linsert $unapplied_list 0 {*}[split $chars ""]]
- set unapplied [join $unapplied_list ""]
-
- break
- }
- }
-
- #todo - consider CSI s DECSLRM vs ansi.sys \x1b\[s - we need \x1b\[s for oldschool ansi art - but may have to enable only for that.
- #we should possibly therefore reverse this mapping so that x1b7 x1b8 are the primary codes for save/restore?
- set code [tcl::string::map [list \x1b7 \x1b\[s \x1b8 \x1b\[u ] $item]
- #since this element isn't a grapheme - advance idx_over to next grapheme overlay when about to fill 'unapplied'
-
-
- #remap of DEC cursor_save/cursor_restore from ESC sequence to equivalent CSI
- #probably not ideal - consider putting cursor_save/cursor_restore in functions so they can be called from the appropriate switch branch instead of using this mapping
- #review - cost/benefit of function calls within these switch-arms instead of inline code?
-
- set c1 [tcl::string::index $code 0]
- set c1c2c3 [tcl::string::range $code 0 2]
- #set re_ST_open {(?:\033P|\u0090|\033X|\u0098|\033\^|\u009e|\033_|\u009f)}
- #tcl 8.7 - faster to use inline list than to store it in a local var outside of loop.
- #(somewhat surprising)
- set leadernorm [tcl::string::range [tcl::string::map [list\
- \x1b\[< 1006\
- \x1b\[ 7CSI\
- \x1bY 7MAP\
- \x1bP 7DCS\
- \x90 8DCS\
- \x9b 8CSI\
- \x1b\] 7OSC\
- \x9d 8OSC\
- \x1b 7ESC\
- ] $c1c2c3] 0 3] ;#leadernorm is 1st 1,2 or 3 chars mapped to 4char normalised indicator - or is original first chars (1,2 or 3 len)
-
- #we leave the tail of the code unmapped for now
- switch -- $leadernorm {
- 1006 {
- #https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
- #SGR (1006) CSI < followed by colon separated encoded-button-value,px,py ordinates and final M for button press m for button release
- set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 3 end]]
- }
- 7CSI - 7OSC {
- #set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 2 end]]
- set codenorm $leadernorm[tcl::string::range $code 2 end]
- }
- 7DCS {
- #ESC P
- #Device Control String https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Controls-beginning-with-ESC:ESC-F.C74
- set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 2 end]]
- }
- 8DCS {
- #8-bit Device Control String
- set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 1 end]]
- }
- 7MAP {
- #map to another type of code to share implementation branch
- set codenorm $leadernorm[tcl::string::range $code 1 end]
- }
- 7ESC {
- #set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 1 end]]
- set codenorm $leadernorm[tcl::string::range $code 1 end]
- }
- 8CSI - 8OSC {
- set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 1 end]]
- }
- default {
- puts stderr "Sequence detected as ANSI, but not handled in leadernorm switch. code: [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- #we haven't made a mapping for this
- #could in theory be 1,2 or 3 in len
- #although we shouldn't be getting here if the regexp for ansi codes is kept in sync with our switch branches
- set codenorm $code
- }
- }
-
- switch -- $leadernorm {
- 7MAP {
- switch -- [lindex $codenorm 4] {
- Y {
- #vt52 movement. we expect 2 chars representing position (limited range)
- set params [tcl::string::range $codenorm 5 end]
- if {[tcl::string::length $params] != 2} {
- #shouldn't really get here or need this branch if ansi splitting was done correctly
- puts stderr "overtype::renderline ESC Y recognised as vt52 move, but incorrect parameters length ([string length $params] vs expected 2) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented codenorm:[ansistring VIEW -lf 1 -vt 1 -nul 1 $codenorm]"
- }
- set line [tcl::string::index $params 5]
- set column [tcl::string::index $params 1]
- set r [expr {[scan $line %c] -31}]
- set c [expr {[scan $column %c] -31}]
-
- #MAP to:
- #CSI n;m H - CUP - Cursor Position
- set leadernorm 7CSI
- set codenorm "$leadernorm${r}\;${c}H"
- }
- }
- }
- }
-
- #we've mapped 7 and 8bit escapes to values we can handle as literals in switch statements to take advantange of jump tables.
- switch -- $leadernorm {
- 1006 {
- #TODO
- #
- switch -- [tcl::string::index $codenorm end] {
- M {
- puts stderr "mousedown $codenorm"
- }
- m {
- puts stderr "mouseup $codenorm"
- }
- }
-
- }
- {7CSI} - {8CSI} {
- set param [tcl::string::range $codenorm 4 end-1]
- #puts stdout "--> CSI [tcl::string::index $leadernorm 0] bit param:$param"
- set code_end [tcl::string::index $codenorm end] ;#used for e.g h|l set/unset mode
-
- switch -exact -- $code_end {
- A {
- #Row move - up
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- #todo
- lassign [split $param {;}] num modifierkey
- if {$modifierkey ne ""} {
- puts stderr "modifierkey:$modifierkey"
- }
-
- if {$num eq ""} {set num 1}
- incr cursor_row -$num
-
- if {$cursor_row < 1} {
- set cursor_row 1
- }
-
- #ensure rest of *overlay* is emitted to remainder
- incr idx_over
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction up
- #retain cursor_column
- break
- }
- B {
- #CUD - Cursor Down
- #Row move - down
- lassign [split $param {;}] num modifierkey
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- #move down
- if {$modifierkey ne ""} {
- puts stderr "modifierkey:$modifierkey"
- }
- if {$num eq ""} {set num 1}
- incr cursor_row $num
-
-
- incr idx_over ;#idx_over hasn't encountered a grapheme and hasn't advanced yet
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction down
- #retain cursor_column
- break
- }
- C {
- #CUF - Cursor Forward
- #Col move
- #puts stdout "->forward"
- #todo - consider right-to-left cursor mode (e.g Hebrew).. some day.
- #cursor forward
- #right-arrow/move forward
- lassign [split $param {;}] num modifierkey
- if {$modifierkey ne ""} {
- puts stderr "modifierkey:$modifierkey"
- }
- if {$num eq ""} {set num 1}
-
- #todo - retrict to moving 1 position past datalen? restrict to column width?
- #should ideally wrap to next line when interactive and not on last row
- #(some ansi art seems to expect this behaviour)
- #This presumably depends on the terminal's wrap mode
- #e.g DECAWM autowrap mode
- # CSI ? 7 h - set: autowrap (also tput smam)
- # CSI ? 7 l - reset: no autowrap (also tput rmam)
- set version 2
- if {$version eq "2"} {
- set max [llength $outcols]
- if {$overflow_idx == -1} {
- incr max
- }
- if {$cursor_column == $max+1} {
- #move_forward while in overflow
- incr cursor_column -1
- }
-
- if {($cursor_column + $num) <= $max} {
- incr idx $num
- incr cursor_column $num
- } else {
- if {$autowrap_mode} {
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- #jmn
- if {$idx == $overflow_idx} {
- incr num
- }
-
- #horizontal movement beyond line extent needs to wrap - throw back to caller
- #we may have both overflow_right and unapplied data
- #(can have overflow_right if we were in insert_mode and processed chars prior to this movement)
- #leave row as is - caller will need to determine how many rows the column-movement has consumed
- incr cursor_column $num ;#give our caller the necessary info as columns from start of row
- #incr idx_over
- #should be gci following last one applied
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction wrapmoveforward
- break
- } else {
- set cursor_column $max
- set idx [expr {$cursor_column -1}]
- }
- }
- } else {
- #review - dead branch
- if {!$expand_right || ($cursor_column + $num) <= [llength $outcols+1]} {
- incr idx $num
- incr cursor_column $num
- } else {
- if {!$insert_mode} {
- #block editing style with arrow keys
- #overtype mode
- set idxstart $idx
- set idxend [llength $outcols]
- set moveend [expr {$idxend - $idxstart}]
- if {$moveend < 0} {set moveend 0} ;#sanity?
- #puts "idxstart:$idxstart idxend:$idxend outcols[llength $outcols] undercols:[llength $undercols]"
- incr idx $moveend
- incr cursor_column $moveend
- #if {[tcl::dict::exists $understacks $idx]} {
- # set stackinfo [tcl::dict::get $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext
- #} else {
- # set stackinfo [list]
- #}
- if {$idx < [llength $understacks]} {
- set stackinfo [lindex $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext
- } else {
- set stackinfo [list]
- }
- if {$idx < [llength $understacks_gx]} {
- #set gxstackinfo [tcl::dict::get $understacks_gx $idx]
- set gxstackinfo [lindex $understacks_gx $idx]
- } else {
- set gxstackinfo [list]
- }
- #pad outcols
- set movemore [expr {$num - $moveend}]
- #assert movemore always at least 1 or we wouldn't be in this branch
- for {set m 1} {$m <= $movemore} {incr m} {
- incr idx
- incr cursor_column
- priv::render_addchar $idx " " $stackinfo $gxstackinfo $insert_mode
- }
- } else {
- #normal - insert
- incr idx $num
- incr cursor_column $num
- if {$idx > [llength $outcols]} {
- set idx [llength $outcols];#allow one beyond - for adding character at end of line
- set cursor_column [expr {[llength $outcols]+1}]
- }
- }
- }
- }
- }
- D {
- #Col move
- #puts stdout "<-back"
- #cursor back
- #left-arrow/move-back when ltr mode
- lassign [split $param {;}] num modifierkey
- if {$modifierkey ne ""} {
- puts stderr "modifierkey:$modifierkey"
- }
- if {$num eq ""} {set num 1}
-
- set version 2
- if {$version eq "2"} {
- #todo - startcolumn offset!
- if {$cursor_column - $num >= 1} {
- incr idx -$num
- incr cursor_column -$num
- } else {
- if {!$autowrap_mode} {
- set cursor_column 1
- set idx 0
- } else {
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- incr cursor_column -$num
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction wrapmovebackward
- break
- }
- }
- } else {
- incr idx -$num
- incr cursor_column -$num
- if {$idx < $opt_colstart-1} {
- #wrap to previous line and position cursor at end of data
- set idx [expr {$opt_colstart-1}]
- set cursor_column $opt_colstart
- }
- }
- }
- E {
- #CNL - Cursor Next Line
- if {$param eq ""} {
- set downmove 1
- } else {
- set downmove [expr {$param}]
- }
- puts stderr "renderline CNL down-by-$downmove"
- set cursor_column 1
- set cursor_row [expr {$cursor_row + $downmove}]
- set idx [expr {$cursor_column -1}]
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- incr idx_over
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction move
- break
-
- }
- F {
- #CPL - Cursor Previous Line
- if {$param eq ""} {
- set upmove 1
- } else {
- set upmove [expr {$param}]
- }
- puts stderr "renderline CPL up-by-$upmove"
- set cursor_column 1
- set cursor_row [expr {$cursor_row -$upmove}]
- if {$cursor_row < 1} {
- set cursor_row 1
- }
- set idx [expr {$cursor_column - 1}]
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- incr idx_over
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction move
- break
-
- }
- G {
- #CHA - Cursor Horizontal Absolute (move to absolute column no)
- if {$param eq ""} {
- set targetcol 1
- } else {
- set targetcol $param
- if {![string is integer -strict $targetcol]} {
- puts stderr "renderline CHA (Cursor Horizontal Absolute) error. Unrecognised parameter '$param'"
- }
- set targetcol [expr {$param}]
- set max [llength $outcols]
- if {$overflow_idx == -1} {
- incr max
- }
- if {$targetcol > $max} {
- puts stderr "renderline CHA (Cursor Horizontal Absolute) error. Param '$param' > max: $max"
- set targetcol $max
- }
- }
- #adjust to colstart - as column 1 is within overlay
- #??? REVIEW
- set idx [expr {($targetcol -1) + $opt_colstart -1}]
-
-
- set cursor_column $targetcol
- #puts stderr "renderline absolute col move ESC G (TEST)"
- }
- H - f {
- #CSI n;m H - CUP - Cursor Position
-
- #CSI n;m f - HVP - Horizontal Vertical Position REVIEW - same as CUP with differences (what?) in some terminal modes
- # - 'counts as effector format function (like CR or LF) rather than an editor function (like CUD or CNL)'
- # - REVIEW
- #see Annex A at: https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf
-
- #test e.g ansicat face_2.ans
- #$re_both_move
- lassign [split $param {;}] paramrow paramcol
- #missing defaults to 1
- #CSI ;5H = CSI 1;5H -> row 1 col 5
- #CSI 17;H = CSI 17H = CSI 17;1H -> row 17 col 1
-
- if {$paramcol eq ""} {set paramcol 1}
- if {$paramrow eq ""} {set paramrow 1}
- if {![string is integer -strict $paramcol] || ![string is integer -strict $paramrow]} {
- puts stderr "renderline CUP (CSI H) unrecognised param $param"
- #ignore?
- } else {
- set max [llength $outcols]
- if {$overflow_idx == -1} {
- incr max
- }
- if {$paramcol > $max} {
- set target_column $max
- } else {
- set target_column [expr {$paramcol}]
- }
-
-
- if {$paramrow < 1} {
- puts stderr "renderline CUP (CSI H) bad row target 0. Assuming 1"
- set target_row 1
- } else {
- set target_row [expr {$paramrow}]
- }
- if {$target_row == $cursor_row} {
- #col move only - no need for break and move
- #puts stderr "renderline CUP col move only to col $target_column param:$param"
- set cursor_column $target_column
- set idx [expr {$cursor_column -1}]
- } else {
- set cursor_row $target_row
- set cursor_column $target_column
- set idx [expr {$cursor_column -1}]
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- incr idx_over
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction move
- break
- }
- }
- }
- J {
- set modegroup [tcl::string::index $codenorm 4] ;#e.g ?
- switch -exact -- $modegroup {
- ? {
- #CSI ? Pn J - selective erase
- puts stderr "overtype::renderline ED - SELECTIVE ERASE IN DISPLAY (UNIMPLEMENTED) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- default {
- puts stderr "overtype::renderline ED - ERASE IN DISPLAY (TESTING) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- if {$param eq ""} {set param 0}
- switch -exact -- $param {
- 0 {
- #clear from cursor to end of screen
- }
- 1 {
- #clear from cursor to beginning of screen
- }
- 2 {
- #clear entire screen
- #ansi.sys - move cursor to upper left REVIEW
- set cursor_row 1
- set cursor_column 1
- set idx [expr {$cursor_column -1}]
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- incr idx_over
- if {[llength $outcols]} {
- priv::render_erasechar 0 [llength $outcols]
- }
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction clear_and_move
- break
- }
- 3 {
- #clear entire screen. presumably cursor doesn't move - otherwise it would be same as 2J ?
-
- }
- default {
- }
- }
-
- }
- }
- }
- K {
- #see DECECM regarding background colour
- set modegroup [tcl::string::index $codenorm 4] ;#e.g ?
- switch -exact -- $modegroup {
- ? {
- puts stderr "overtype::renderline DECSEL - SELECTIVE ERASE IN LINE (UNIMPLEMENTED) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- set param [string range $param 1 end] ;#chop qmark
- if {$param eq ""} {set param 0}
- switch -exact -- $param {
- 0 {
- #clear from cursor to end of line - depending on DECSCA
- }
- 1 {
- #clear from cursor to beginning of line - depending on DECSCA
-
- }
- 2 {
- #clear entire line - depending on DECSCA
- }
- default {
- puts stderr "overtype::renderline DECSEL - SELECTIVE ERASE IN LINE PARAM '$param' unrecognised [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- }
-
- }
- default {
- puts stderr "overtype::renderline EL - ERASE IN LINE (TESTING) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- if {$param eq ""} {set param 0}
- switch -exact -- $param {
- 0 {
- #clear from cursor to end of line
- }
- 1 {
- #clear from cursor to beginning of line
-
- }
- 2 {
- #clear entire line
- }
- default {
- puts stderr "overtype::renderline EL - ERASE IN LINE PARAM '$param' unrecognised [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- }
- }
- }
- }
- L {
- puts stderr "overtype::renderline IL - Insert Line - not implemented [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- M {
- #CSI Pn M - DL - Delete Line
- puts stderr "overtype::renderline DL - Delete Line - not implemented [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
-
- }
- T {
- #CSI Pn T - SD Pan Up (empty lines introduced at top)
- #CSI Pn+T - kitty extension (lines at top come from scrollback buffer)
- #Pn new lines appear at top of the display, Pn old lines disappear at the bottom of the display
- if {$param eq "" || $param eq "0"} {set param 1}
- if {[string index $param end] eq "+"} {
- puts stderr "overtype::renderline CSI Pn + T - kitty Pan Up - not implemented [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- } else {
- puts stderr "overtype::renderline CSI Pn T - SD Pan Up - not implemented [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- }
- X {
- puts stderr "overtype::renderline X ECH ERASE CHARACTER - $param"
- #ECH - erase character
- if {$param eq "" || $param eq "0"} {set param 1}; #param=count of chars to erase
- priv::render_erasechar $idx $param
- #cursor position doesn't change.
- }
- q {
- set code_secondlast [tcl::string::index $codenorm end-1]
- switch -exact -- $code_secondlast {
- {"} {
- #DECSCA - Select Character Protection Attribute
- #(for use with selective erase: DECSED and DECSEL)
- set param [tcl::string::range $codenorm 4 end-2]
- if {$param eq ""} {set param 0}
- #TODO - store like SGR in stacks - replays?
- switch -exact -- $param {
- 0 - 2 {
- #canerase
- puts stderr "overtype::renderline - DECSCA canerase not implemented - [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- 1 {
- #cannoterase
- puts stderr "overtype::renderline - DECSCA cannoterase not implemented - [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- default {
- puts stderr "overtype::renderline DECSCA param '$param' not understood [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- }
-
- }
- default {
- puts stderr "overtype::renderline - CSI ... q not implemented - [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- }
-
- }
- r {
- #$re_decstbm
- #https://www.vt100.net/docs/vt510-rm/DECSTBM.html
- #This control function sets the top and bottom margins for the current page. You cannot perform scrolling outside the margins
- lassign [split $param {;}] margin_top margin_bottom
-
- #todo - return these for the caller to process..
- puts stderr "overtype::renderline DECSTBM set top and bottom margin not implemented"
- #Also moves the cursor to col 1 line 1 of the page
- set cursor_column 1
- set cursor_row 1
-
- incr idx_over
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction move ;#own instruction? decstbm?
- break
- }
- s {
- #code conflict between ansi emulation and DECSLRM - REVIEW
- #ANSISYSSC (when no parameters) - like other terminals - essentially treat same as DECSC
- # todo - when parameters - support DECSLRM instead
-
- if {$param ne ""} {
- #DECSLRM - should only be recognised if DECLRMM is set (vertical split screen mode)
- lassign [split $param {;} margin_left margin_right
- puts stderr "overtype DECSLRM not yet supported - got [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- if {$margin_left eq ""} {
- set margin_left 1
- }
- set columns_per_page 80 ;#todo - set to 'page width (DECSCPP set columns per page)' - could be 132 or??
- if {$margin_right eq ""} {
- set margin_right $columns_per_page
- }
- puts stderr "DECSLRM margin left: $margin_left margin right: $margin_right"
- if {![string is integer -strict $margin_left] || $margin_left < 0} {
- puts stderr "DECSLRM invalid margin_left"
- }
- if {![string is integer -strict $margin_right] || $margin_right < 0} {
- puts stderr "DECSLRM invalid margin_right"
- }
- set scrolling_region_size [expr {$margin_right - $margin_left}]
- if {$scrolling_region_size < 2 || $scrolling_region_size > $columns_per_page} {
- puts stderr "DECSLRM region size '$scrolling_regsion_size' must be between 1 and $columns_per_page"
- }
- #todo
-
-
- } else {
- #DECSC
- #//notes on expected behaviour:
- #DECSC - saves following items in terminal's memory
- #cursor position
- #character attributes set by the SGR command
- #character sets (G0,G1,G2 or G3) currently in GL and GR
- #Wrap flag (autowrap or no autowrap)
- #State of origin mode (DECOM)
- #selective erase attribute
- #any single shift 2 (SS2) or single shift 3(SSD) functions sent
-
- #$re_cursor_save
- #cursor save could come after last column
- if {$overflow_idx != -1 && $idx == $overflow_idx} {
- #bartman2.ans test file - fixes misalignment at bottom of dialog bubble
- #incr cursor_row
- #set cursor_column 1
- #bwings1.ans test file - breaks if we actually incr cursor (has repeated saves)
- set cursor_saved_position [list row [expr {$cursor_row+1}] column 1]
- } else {
- set cursor_saved_position [list row $cursor_row column $cursor_column]
- }
- #there may be overlay stackable codes emitted that aren't in the understacks because they come between the last emmited character and the cursor_save control.
- #we need the SGR and gx overlay codes prior to the cursor_save
-
- #a real terminal would not be able to know the state of the underlay.. so we should probably ignore it.
- #set sgr_stack [lindex $understacks $idx]
- #set gx_stack [lindex $understacks_gx $idx] ;#not actually a stack - just a boolean state (for now?)
-
- set sgr_stack [list]
- set gx_stack [list]
-
- #we shouldn't need to scan for intermediate cursor save/restores - as restores would throw-back to the calling loop - so our overlay 'line' is since those.
- #The overlay_grapheme_control_list had leading resets from previous lines - so we go back to the beginning not just the first grapheme.
-
- foreach gc [lrange $overlay_grapheme_control_list 0 $gci-1] {
- lassign $gc type code
- #types g other sgr gx0
- switch -- $type {
- gx0 {
- #code is actually a stand-in for the graphics on/off code - not the raw code
- #It is either gx0_on or gx0_off
- set gx_stack [list $code]
- }
- sgr {
- #code is the raw code
- if {[punk::ansi::codetype::is_sgr_reset $code]} {
- #jmn
- set sgr_stack [list "\x1b\[m"]
- } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} {
- set sgr_stack [list $code]
- lappend overlay_grapheme_control_list [list sgr $code]
- } elseif {[priv::is_sgr $code]} {
- #often we don't get resets - and codes just pile up.
- #as a first step to simplifying - at least remove earlier straight up dupes
- set dup_posns [lsearch -all -exact $sgr_stack $code] ;#needs -exact - codes have square-brackets (glob chars)
- set sgr_stack [lremove $sgr_stack {*}$dup_posns]
- lappend sgr_stack $code
- }
- }
- }
- }
- set cursor_saved_attributes ""
- switch -- [lindex $gx_stack 0] {
- gx0_on {
- append cursor_saved_attributes "\x1b(0"
- }
- gx0_off {
- append cursor_saved_attributes "\x1b(B"
- }
- }
- #append cursor_saved_attributes [join $sgr_stack ""]
- append cursor_saved_attributes [punk::ansi::codetype::sgr_merge_list {*}$sgr_stack]
-
- #as there is apparently only one cursor storage element we don't need to throw back to the calling loop for a save.
-
- #don't incr index - or the save will cause cursor to move to the right
- #carry on
- }
- }
- u {
- #ANSISYSRC save cursor (when no parameters) (DECSC)
-
- #$re_cursor_restore
- #we are going to jump somewhere.. for now we will assume another line, and process accordingly.
- #The caller has the cursor_saved_position/cursor_saved_attributes if any (?review - if we always pass it back it, we could save some calls for moves in same line)
- #don't set overflow at this point. The existing underlay to the right must be preserved.
- #we only want to jump and render the unapplied at the new location.
-
- #lset overstacks $idx_over [list]
- #set replay_codes_overlay ""
-
- #if {$cursor_saved_attributes ne ""} {
- # set replay_codes_overlay $cursor_saved_attributes ;#empty - or last save if it happend in this input chunk
- #} else {
- #jj
- #set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- set replay_codes_overlay ""
- #}
-
- #like priv::render_unapplied - but without the overlay's ansi reset or gx stacks from before the restore code
- incr idx_over
-
- set unapplied ""
- set unapplied_list [list]
- foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] {
- lassign $gc type item
- if {$type eq "gx0"} {
- if {$item eq "gx0_on"} {
- lappend unapplied_list "\x1b(0"
- } elseif {$item eq "gx0_off"} {
- lappend unapplied_list "\x1b(B"
- }
- } else {
- lappend unapplied_list $item
- }
- #incr idx_over
- }
- set unapplied [join $unapplied_list ""]
- #if the save occured within this line - that's ok - it's in the return value list and caller can prepend for the next loop.
- set instruction restore_cursor
- break
- }
- "{" {
-
- puts stderr "renderline warning - CSI.. - unimplemented [ansistring VIEW -lf 1 -nul 1 $code]"
- }
- "}" {
- set code_secondlast [tcl::string::index $codenorm end-1]
- switch -exact -- $code_secondlast {
- ' {
- puts stderr "renderline warning - DECIC - Insert Column - CSI...' - unimplemented [ansistring VIEW -lf 1 -nul 1 $code]"
- }
- default {
- puts stderr "renderline warning - CSI.. - unimplemented [ansistring VIEW -lf 1 -nul 1 $code]"
- }
- }
- }
- ~ {
- set code_secondlast [tcl::string::index $codenorm end-1] ;#used for e.g CSI x '~
- switch -exact -- $code_secondlast {
- ' {
- #DECDC - editing sequence - Delete Column
- puts stderr "renderline warning - DECDC - unimplemented"
- }
- default {
- #$re_vt_sequence
- lassign [split $param {;}] key mod
-
- #Note that f1 to f4 show as ESCOP|Q|R|S (VT220?) but f5+ show as ESC\[15~
- #
- #e.g esc \[2~ insert esc \[2;2~ shift-insert
- #mod - subtract 1, and then use bitmask
- #shift = 1, (left)Alt = 2, control=4, meta=8 (meta seems to do nothing on many terminals on windows? Intercepted by windows?)
- #puts stderr "vt key:$key mod:$mod code:[ansistring VIEW $code]"
- if {$key eq "1"} {
- #home
- } elseif {$key eq "2"} {
- #Insert
- if {$mod eq ""} {
- #no modifier key
- set insert_mode [expr {!$insert_mode}]
- #rather than set the cursor - we return the insert mode state so the caller can decide
- }
- } elseif {$key eq "3"} {
- #Delete - presumably this shifts other chars in the line, with empty cells coming in from the end
- switch -- $mod {
- "" {
- priv::render_delchar $idx
- }
- "5" {
- #ctrl-del - delete to end of word (pwsh) - possibly word on next line if current line empty(?)
- }
- }
- } elseif {$key eq "4"} {
- #End
- } elseif {$key eq "5"} {
- #pgup
- } elseif {$key eq "6"} {
- #pgDn
- } elseif {$key eq "7"} {
- #Home
- #??
- set idx [expr {$opt_colstart -1}]
- set cursor_column 1
- } elseif {$key eq "8"} {
- #End
- } elseif {$key eq "11"} {
- #F1 - or ESCOP or e.g shift F1 ESC\[1;2P
- } elseif {$key eq "12"} {
- #F2 - or ESCOQ
- } elseif {$key eq "13"} {
- #F3 - or ESCOR
- } elseif {$key eq "14"} {
- #F4 - or ESCOS
- } elseif {$key eq "15"} {
- #F5 or shift F5 ESC\[15;2~
- } elseif {$key eq "17"} {
- #F6
- } elseif {$key eq "18"} {
- #F7
- } elseif {$key eq "19"} {
- #F8
- } elseif {$key eq "20"} {
- #F9
- } elseif {$key eq "21"} {
- #F10
- } elseif {$key eq "23"} {
- #F11
- } elseif {$key eq "24"} {
- #F12
- }
-
- }
- }
-
- }
- h - l {
- #set mode unset mode
- #we are matching only last char to get to this arm - but are there other sequences ending in h|l we need to handle?
-
- #$re_mode if first after CSI is "?"
- #some docs mention ESC=h|l - not seen on windows terminals.. review
- #e.g https://www2.math.upenn.edu/~kazdan/210/computer/ansi.html
- set modegroup [tcl::string::index $codenorm 4] ;#e.g ? =
- switch -exact -- $modegroup {
- ? {
- set smparams [tcl::string::range $codenorm 5 end-1] ;#params between ? and h|l
- #one or more modes can be set
- set smparam_list [split $smparams {;}]
- foreach num $smparam_list {
- switch -- $num {
- "" {
- #ignore empties e.g extra/trailing semicolon in params
- }
- 5 {
- #DECSNM - reverse video
- #How we simulate this to render within a block of text is an open question.
- #track all SGR stacks and constantly flip based on the current SGR reverse state?
- #It is the job of the calling loop to do this - so at this stage we'll just set the states
-
- if {$code_end eq "h"} {
- #set (enable)
- set reverse_mode 1
- } else {
- #reset (disable)
- set reverse_mode 0
- }
-
- }
- 7 {
- #DECAWM autowrap
- if {$code_end eq "h"} {
- #set (enable)
- set autowrap_mode 1
- if {$opt_width ne "\uFFEF"} {
- set overflow_idx $opt_width
- } else {
- #review - this is also the cursor position when adding a char at end of line?
- set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it
- }
- #review - can idx ever be beyond overflow_idx limit when we change e.g with a width setting and cursor movements?
- # presume not usually - but sanity check with warning for now.
- if {$idx >= $overflow_idx} {
- puts stderr "renderline warning - idx '$idx' >= overflow_idx '$overflow_idx' - unexpected"
- }
- } else {
- #reset (disable)
- set autowrap_mode 0
- #REVIEW!
- set overflow_idx -1
- }
- }
- 25 {
- if {$code_end eq "h"} {
- #visible cursor
-
- } else {
- #invisible cursor
-
- }
- }
- 117 {
- #DECECM - Erase Color Mode
- #https://invisible-island.net/ncurses/ncurses.faq.html
- #The Erase color selection controls the background color used when text is erased or new
- #text is scrolled on to the screen. Screen background causes newly erased areas or
- #scrolled text to be written using color index zero, the screen background. This is VT
- #and DECterm compatible. Text background causes erased areas or scrolled text to be
- #written using the current text background color. This is PC console compatible and is
- #the factory default.
-
- #see also: https://unix.stackexchange.com/questions/251726/clear-to-end-of-line-uses-the-wrong-background-color-in-screen
- }
- }
- }
- }
- = {
- set num [tcl::string::range $codenorm 5 end-1] ;#param between = and h|l
- puts stderr "overtype::renderline CSI=...h|l code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented"
- }
- default {
- #e.g CSI 4 h
- set num [tcl::string::range $codenorm 4 end-1] ;#param before h|l
- switch -exact -- $num {
- 3 {
- puts stderr "CRM MODE $code_end"
- #CRM - Show control character mode
- # 'No control functions are executed except LF,FF and VT which are represented in the CRM FONT before a CRLF(new line) is executed'
- #
- #use ansistring VIEW -nul 1 -lf 2 -ff 2 -vt 2
- #https://vt100.net/docs/vt510-rm/CRM.html
- #NOTE - vt100 CRM always does auto-wrap at right margin.
- #disabling auto-wrap in set-up or by sequence is disabled.
- #We should default to turning off auto-wrap when crm_mode enabled.. but
- #displaying truncated (on rhs) crm can still be very useful - and we have optimisation in overflow to avoid excess renderline calls (per grapheme)
- #we therefore could reasonably put in an exception to allow auto_wrap to be disabled after crm_mode is engaged,
- #although this would be potentially an annoying difference to some.. REVIEW
- if {$code_end eq "h"} {
- set crm_mode 1
- set autowrap_mode 1
- if {$opt_width ne "\uFFEF"} {
- set overflow_idx $opt_width
- } else {
- #review - this is also the cursor position when adding a char at end of line?
- set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it
- }
- } else {
- set crm_mode 0
- }
- }
- 4 {
- #IRM - Insert/Replace Mode
- if {$code_end eq "h"} {
- #CSI 4 h
- set insert_mode 1
- } else {
- #CSI 4 l
- #replace mode
- set insert_mode 0
- }
- }
- default {
- puts stderr "overtype::renderline CSI...h|l code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented"
- }
- }
- }
- }
- }
- | {
- switch -- [tcl::string::index $codenorm end-1] {
- {$} {
- #CSI ... $ | DECSCPP set columns per page- (recommended in vt510 docs as preferable to DECCOLM)
- #real terminals generally only supported 80/132
- #some other virtuals support any where from 2 to 65,536?
- #we will allow arbitrary widths >= 2 .. to some as yet undetermined limit.
- #CSI $ |
- #empty or 0 param is 80 for compatibility - other numbers > 2 accepted
- set page_width -1 ;#flag as unset
- if {$param eq ""} {
- set page_width 80
- } elseif {[string is integer -strict $param] && $param >=2 0} {
- set page_width [expr {$param}] ;#we should allow leading zeros in the number - but lets normalize using expr
- } else {
- puts stderr "overtype::renderline unacceptable DECSPP value '$param'"
- }
-
- if {$page_width > 2} {
- puts stderr "overtype::renderline DECSCPP - not implemented - but selected width '$page_width' looks ok"
- #if cursor already beyond new page_width - will move to right colum - otherwise no cursor movement
-
- }
-
- }
- default {
- puts stderr "overtype::renderline unrecognised CSI code ending in pipe (|) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented"
- }
- }
- }
- default {
- puts stderr "overtype::renderline CSI code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented"
- }
- }
- }
- 7ESC {
- #
- #re_other_single {\x1b(D|M|E)$}
- #also vt52 Y..
- #also PM \x1b^...(ST)
- switch -- [tcl::string::index $codenorm 4] {
- c {
- #RIS - reset terminal to initial state - where 'terminal' in this case is the renderspace - not the underlying terminal!
- puts stderr "renderline reset"
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction reset
- break
- }
- D {
- #\x84
- #index (IND)
- #vt102-docs: "Moves cursor down one line in same column. If cursor is at bottom margin, screen performs a scroll-up"
- puts stderr "renderline ESC D not fully implemented"
- incr cursor_row
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction down
- #retain cursor_column
- break
- }
- E {
- #\x85
- #review - is behaviour different to lf?
- #todo - possibly(?) same logic as handling above. i.e return instruction depends on where column_cursor is at the time we get NEL
- #leave implementation until logic for is set in stone... still under review
- #It's arguable NEL is a pure cursor movement as opposed to the semantic meaning of crlf or lf in a file.
- #
- #Next Line (NEL) "Move the cursor to the left margin on the next line. If the cursor is at the bottom margin, scroll the page up"
- puts stderr "overtype::renderline ESC E unimplemented"
-
- }
- H {
- #\x88
- #Tab Set
- puts stderr "overtype::renderline ESC H tab set unimplemented"
- }
- M {
- #\x8D
- #Reverse Index (RI)
- #vt102-docs: "Moves cursor up one line in same column. If cursor is at top margin, screen performs a scroll-down"
- puts stderr "overtype::renderline ESC M not fully implemented"
-
- set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]]
- #move up
- incr cursor_row -1
- if {$cursor_row < 1} {
- set cursor_row 1
- }
- #ensure rest of *overlay* is emitted to remainder
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction up ;#need instruction for scroll-down?
- #retain cursor_column
- break
- }
- N {
- #\x8e - affects next character only
- puts stderr "overtype::renderline single shift select G2 command UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- O {
- #\x8f - affects next character only
- puts stderr "overtype::renderline single shift select G3 command UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- P {
- #\x90
- #DCS - shouldn't get here - handled in 7DCS branch
- #similarly \] OSC (\x9d) and \\ (\x9c) ST
- }
- V {
- #\x96
-
- }
- W {
- #\x97
- }
- X {
- #\x98
- #SOS
- if {[string index $code end] eq "\007"} {
- set sos_content [string range $code 2 end-1] ;#ST is \007
- } else {
- set sos_content [string range $code 2 end-2] ;#ST is \x1b\\
- }
- #return in some useful form to the caller
- #TODO!
- lappend sos_list [list string $sos_content row $cursor_row column $cursor_column]
- puts stderr "overtype::renderline ESCX SOS UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- ^ {
- #puts stderr "renderline PM"
- #Privacy Message.
- if {[string index $code end] eq "\007"} {
- set pm_content [string range $code 2 end-1] ;#ST is \007
- } else {
- set pm_content [string range $code 2 end-2] ;#ST is \x1b\\
- }
- #We don't want to render it - but we need to make it available to the application
- #see the textblock library in punk, for the exception we make here for single backspace.
- #It is unlikely to be encountered as a useful PM - so we hack to pass it through as a fix
- #for spacing issues on old terminals which miscalculate the single-width 'Symbols for Legacy Computing'
- if {$pm_content eq "\b"} {
- #puts stderr "renderline PM sole backspace special handling for \U1FB00 - \U1FBFF"
- #esc^\b\007 or esc^\besc\\
- #HACKY pass-through - targeting terminals that both mis-space legacy symbols *and* don't support PMs
- #The result is repair of the extra space. If the terminal is a modern one and does support PM - the \b should be hidden anyway.
- #If the terminal has the space problem AND does support PMs - then this just won't fix it.
- #The fix relies on the symbol-supplier to cooperate by appending esc^\b\esc\\ to the problematic symbols.
-
- #priv::render_addchar $idx $code [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode
- #idx has been incremented after last grapheme added
- priv::render_append_to_char [expr {$idx -1}] $code
- }
- #lappend to a dict element in the result for application-specific processing
- lappend pm_list $pm_content
- }
- _ {
- #APC Application Program Command
- #just warn for now..
- puts stderr "overtype::renderline ESC_ APC command UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- default {
- puts stderr "overtype::renderline ESC code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented codenorm:[ansistring VIEW -lf 1 -vt 1 -nul 1 $codenorm]"
- }
- }
-
- }
- 7DCS - 8DCS {
- puts stderr "overtype::renderline DCS - DEVICE CONTROL STRING command UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- #ST (string terminator) \x9c or \x1b\\
- if {[tcl::string::index $codenorm end] eq "\x9c"} {
- set code_content [tcl::string::range $codenorm 4 end-1] ;#ST is 8-bit 0x9c
- } else {
- set code_content [tcl::string::range $codenorm 4 end-2] ;#ST is \x1b\\
- }
-
- }
- 7OSC - 8OSC {
- # OSCs are terminated with ST of either \007 or \x1b\\ - we allow either whether code was 7 or 8 bit
- if {[tcl::string::index $codenorm end] eq "\007"} {
- set code_content [tcl::string::range $codenorm 4 end-1] ;#ST is \007
- } else {
- set code_content [tcl::string::range $codenorm 4 end-2] ;#ST is \x1b\\
- }
- set first_colon [tcl::string::first {;} $code_content]
- if {$first_colon == -1} {
- #there probably should always be a colon - but we'll try to make sense of it without
- set osc_code $code_content ;#e.g \x1b\]104\007 vs \x1b\]104\;\007
- } else {
- set osc_code [tcl::string::range $code_content 0 $first_colon-1]
- }
- switch -exact -- $osc_code {
- 2 {
- set newtitle [tcl::string::range $code_content 2 end]
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction [list set_window_title $newtitle]
- break
- }
- 4 {
- #OSC 4 - set colour palette
- #can take multiple params
- #e.g \x1b\]4\;1\;red\;2\;green\x1b\\
- set params [tcl::string::range $code_content 2 end] ;#strip 4 and first semicolon
- set cmap [dict create]
- foreach {cnum spec} [split $params {;}] {
- if {$cnum >= 0 and $cnum <= 255} {
- #todo - parse spec from names like 'red' to RGB
- #todo - accept rgb:ab/cd/ef as well as rgb:/a/b/c (as alias for aa/bb/cc)
- #also - what about rgb:abcd/defg/hijk and 12-bit abc/def/ghi ?
- dict set cmap $cnum $spec
- } else {
- #todo - log
- puts stderr "overtype::renderline OSC 4 set colour palette - bad color number: $cnum must be from 0 to 255. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- }
-
- puts stderr "overtype::renderline OSC 4 set colour palette unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
-
-
- }
- 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 {
- #OSC 10 through 17 - so called 'dynamic colours'
- #can take multiple params - each successive parameter changes the next colour in the list
- #- e.g if code started at 11 - next param is for 12. 17 takes only one param because there are no more
- #10 change text foreground colour
- #11 change text background colour
- #12 change text cursor colour
- #13 change mouse foreground colour
- #14 change mouse background colour
- #15 change tektronix foreground colour
- #16 change tektronix background colour
- #17 change highlight colour
- set params [tcl::string::range $code_content 2 end]
-
- puts stderr "overtype::renderline OSC $osc_code set dynamic colours unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
-
-
- }
- 18 {
- #why is this not considered one of the dynamic colours above?
- #https://www.xfree86.org/current/ctlseqs.html
- #tektronix cursor color
- puts stderr "overtype::renderline OSC 18 - set tektronix cursor color unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- 99 {
- #kitty desktop notifications
- #https://sw.kovidgoyal.net/kitty/desktop-notifications/
- # 99 ; metadata ; payload
- puts stderr "overtype::renderline OSC 99 kitty desktop notification unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- 104 {
- #reset colour palette
- #we want to do it for the current rendering context only (vvt) - not just pass through to underlying vt
- puts stderr "overtype::renderline OSC 104 reset colour palette unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- priv::render_unapplied $overlay_grapheme_control_list $gci
- set instruction [list reset_colour_palette]
- break
- }
- 1337 {
- #iterm2 graphics and file transfer
- puts stderr "overtype::renderline OSC 1337 iterm2 graphics/file_transfer unimplemented. 1st 100 chars of code [ansistring VIEW -lf 1 -vt 1 -nul 1 [string range $code 0 99]]"
- }
- 5113 {
- puts stderr "overtype::renderline OSC 5113 kitty file transfer unimplemented. 1st 100 chars of code [ansistring VIEW -lf 1 -vt 1 -nul 1 [string range $code 0 99]]"
- }
- default {
- puts stderr "overtype::renderline OSC - UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]"
- }
- }
-
- }
- default {
- }
- }
-
-
- }
- default {
- #don't need to handle sgr or gx0 types
- #we have our sgr gx0 codes already in stacks for each overlay grapheme
- }
- }
- }
-
- #--------
- if {$opt_expand_right == 0} {
- #need to truncate to the width of the original undertext
- #review - string_width vs printing_length here. undertext requirement to be already rendered therefore punk::char::string_width ok?
- #set num_under_columns [punk::char::string_width $pt_underchars] ;#plaintext underchars
- }
- if {$overflow_idx == -1} {
- #overflow was initially unlimited and hasn't been overridden
- } else {
-
- }
- #--------
-
-
- #coalesce and replay codestacks for outcols grapheme list
- set outstring "" ;#output prior to overflow
- set overflow_right "" ;#remainder after overflow point reached
- set i 0
- set cstack [list]
- set prevstack [list]
- set prev_g0 [list]
- #note overflow_idx may already have been set lower if we had a row move above due to \v or ANSI moves
- set in_overflow 0 ;#used to stop char-width scanning once in overflow
- if {$overflow_idx == 0} {
- #how does caller avoid an infinite loop if they have autowrap on and keep throwing graphemes to the next line? REVIEW
- set in_overflow 1
- }
- set trailing_nulls 0
- foreach ch [lreverse $outcols] {
- if {$ch eq "\u0000"} {
- incr trailing_nulls
- } else {
- break
- }
- }
- if {$trailing_nulls} {
- set first_tail_null_posn [expr {[llength $outcols] - $trailing_nulls}]
- } else {
- set first_tail_null_posn -1
- }
-
- #puts stderr "first_tail_null_posn: $first_tail_null_posn"
- #puts stderr "colview: [ansistring VIEW $outcols]"
-
- foreach ch $outcols {
- #puts "---- [ansistring VIEW $ch]"
-
- set gxleader ""
- if {$i < [llength $understacks_gx]} {
- #set g0 [tcl::dict::get $understacks_gx $i]
- set g0 [lindex $understacks_gx $i]
- if {$g0 ne $prev_g0} {
- if {$g0 eq [list "gx0_on"]} {
- set gxleader "\x1b(0"
- } else {
- set gxleader "\x1b(B"
- }
- }
- set prev_g0 $g0
- } else {
- set prev_g0 [list]
- }
-
- set sgrleader ""
- if {$i < [llength $understacks]} {
- #set cstack [tcl::dict::get $understacks $i]
- set cstack [lindex $understacks $i]
- if {$cstack ne $prevstack} {
- if {[llength $prevstack] && ![llength $cstack]} {
- #This reset is important e.g testfile fruit.ans - we get overhang on rhs without it. But why is cstack empty?
- append sgrleader \033\[m
- } else {
- append sgrleader [punk::ansi::codetype::sgr_merge_list {*}$cstack]
- }
- }
- set prevstack $cstack
- } else {
- set prevstack [list]
- }
-
-
-
- if {$in_overflow} {
- if {$i == $overflow_idx} {
- set 0 [lindex $understacks_gx $i]
- set gxleader ""
- if {$g0 eq [list "gx0_on"]} {
- set gxleader "\x1b(0"
- } elseif {$g0 eq [list "gx0_off"]} {
- set gxleader "\x1b(B"
- }
- append overflow_right $gxleader
- set cstack [lindex $understacks $i]
- set sgrleader ""
- #whether cstack is same or differs from previous char's stack - we must have an output at the start of the overflow_right
- #if {[llength $prevstack] && ![llength $cstack]} {
- # append sgrleader \033\[m
- #}
- append sgrleader [punk::ansi::codetype::sgr_merge_list {*}$cstack]
- append overflow_right $sgrleader
- append overflow_right $ch
- } else {
- append overflow_right $gxleader
- append overflow_right $sgrleader
- append overflow_right $ch
- }
- } else {
- if {$overflow_idx != -1 && $i+1 == $overflow_idx} {
- #one before overflow
- #will be in overflow in next iteration
- set in_overflow 1
- if {[grapheme_width_cached $ch]> 1} {
- #we overflowed with second-half of a double-width char - replace first-half with user-supplied exposition char (should be 1 wide)
- set ch $opt_exposed1
- }
- }
- append outstring $gxleader
- append outstring $sgrleader
- if {$ch eq "\u0000"} {
- if {$cp437_glyphs} {
- #map all nulls including at tail to space
- append outstring " "
- } else {
- if {$trailing_nulls && $i < $first_tail_null_posn} {
- append outstring " " ;#map inner nulls to space
- } else {
- append outstring \u0000
- }
- }
- } else {
- append outstring $ch
- }
- }
- incr i
- }
- #flower.ans good test for null handling - reverse line building
- #review - presence of overflow_right doesn't indicate line's trailing nulls should remain.
- #The cells could have been erased?
- #if {!$cp437_glyphs} {
- # #if {![ansistring length $overflow_right]} {
- # # set outstring [tcl::string::trimright $outstring "\u0000"]
- # #}
- # set outstring [tcl::string::trimright $outstring "\u0000"]
- # set outstring [tcl::string::map {\u0000 " "} $outstring]
- #}
-
-
- #REVIEW
- #set overflow_right [tcl::string::trimright $overflow_right "\u0000"]
- #set overflow_right [tcl::string::map {\u0000 " "} $overflow_right]
-
- set replay_codes ""
- if {[llength $understacks] > 0} {
- if {$overflow_idx == -1} {
- #set tail_idx [tcl::dict::size $understacks]
- set tail_idx [llength $understacks]
- } else {
- set tail_idx [llength $undercols]
- }
- if {$tail_idx-1 < [llength $understacks]} {
- #set replay_codes [join [lindex $understacks $tail_idx-1] ""] ;#tail replay codes
- set replay_codes [punk::ansi::codetype::sgr_merge_list {*}[lindex $understacks $tail_idx-1]] ;#tail replay codes
- }
- if {$tail_idx-1 < [llength $understacks_gx]} {
- set gx0 [lindex $understacks_gx $tail_idx-1]
- if {$gx0 eq [list "gx0_on"]} {
- #if it was on, turn gx0 off at the point we stop processing overlay
- append outstring "\x1b(B"
- }
- }
- }
- if {[string length $overflow_right]} {
- #puts stderr "remainder:$overflow_right"
- }
- #pdict $understacks
-
- if {[punk::ansi::ta::detect_sgr $outstring]} {
- append outstring [punk::ansi::a] ;#without this - we would get for example, trailing backgrounds after rightmost column
-
- #close off any open gx?
- #probably should - and overflow_right reopen?
- }
-
- if {$opt_returnextra} {
- #replay_codes is the codestack at the boundary - used for ellipsis colouring to match elided text - review
- #replay_codes_underlay is the set of codes in effect at the very end of the original underlay
-
- #review
- #replay_codes_overlay is the set of codes in effect at the very end of the original overlay (even if not all overlay was applied)
- #todo - replay_codes for gx0 mode
-
- #overflow_idx may change during ansi & character processing
- if {$overflow_idx == -1} {
- set overflow_right_column ""
- } else {
- set overflow_right_column [expr {$overflow_idx+1}]
- }
- set result [tcl::dict::create\
- result $outstring\
- visualwidth [punk::ansi::printing_length $outstring]\
- instruction $instruction\
- stringlen [string length $outstring]\
- overflow_right_column $overflow_right_column\
- overflow_right $overflow_right\
- unapplied $unapplied\
- unapplied_list $unapplied_list\
- insert_mode $insert_mode\
- autowrap_mode $autowrap_mode\
- crm_mode $crm_mode\
- reverse_mode $reverse_mode\
- insert_lines_above $insert_lines_above\
- insert_lines_below $insert_lines_below\
- cursor_saved_position $cursor_saved_position\
- cursor_saved_attributes $cursor_saved_attributes\
- cursor_column $cursor_column\
- cursor_row $cursor_row\
- expand_right $opt_expand_right\
- replay_codes $replay_codes\
- replay_codes_underlay $replay_codes_underlay\
- replay_codes_overlay $replay_codes_overlay\
- pm_list $pm_list\
- ]
- if {$opt_returnextra == 1} {
- #puts stderr "renderline: $result"
- return $result
- } else {
- #human/debug - map special chars to visual glyphs
- set viewop VIEW
- switch -- $opt_returnextra {
- 2 {
- #codes and character data
- set viewop VIEWCODES ;#ansi colorisation of codes - green for SGR, blue/blue reverse for cursor_save/cursor_restore, cyan for movements, orange for others
- }
- 3 {
- set viewop VIEWSTYLE ;#ansi colorise the characters within the output with preceding codes, stacking codes only within each dict value - may not be same SGR effect as the effect in-situ.
- }
- }
- tcl::dict::set result result [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result result]]
- tcl::dict::set result overflow_right [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result overflow_right]]
- tcl::dict::set result unapplied [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result unapplied]]
- tcl::dict::set result unapplied_list [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result unapplied_list]]
- tcl::dict::set result replay_codes [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result replay_codes]]
- tcl::dict::set result replay_codes_underlay [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result replay_codes_underlay]]
- tcl::dict::set result replay_codes_overlay [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result replay_codes_overlay]]
- tcl::dict::set result cursor_saved_attributes [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result cursor_saved_attributes]]
- return $result
- }
- } else {
- #puts stderr "renderline returning: result $outstring instruction $instruction unapplied $unapplied overflow_right $overflow_right"
- return $outstring
- }
- #return [join $out ""]
- }
-
- #*** !doctools
- #[list_end] [comment {--- end definitions namespace overtype ---}]
-}
-
-tcl::namespace::eval overtype::piper {
- proc overcentre {args} {
- if {[llength $args] < 2} {
- error {usage: ?-bias left|right? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata}
- }
- lassign [lrange $args end-1 end] over under
- set argsflags [lrange $args 0 end-2]
- tailcall overtype::centre {*}$argsflags $under $over
- }
- proc overleft {args} {
- if {[llength $args] < 2} {
- error {usage: ?-startcolumn ? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata}
- }
- lassign [lrange $args end-1 end] over under
- set argsflags [lrange $args 0 end-2]
- tailcall overtype::left {*}$argsflags $under $over
- }
-}
-
-
-# -- --- --- --- --- --- --- --- --- --- ---
-proc overtype::transparentline {args} {
- foreach {under over} [lrange $args end-1 end] break
- set argsflags [lrange $args 0 end-2]
- set defaults [tcl::dict::create\
- -transparent 1\
- -exposed 1 " "\
- -exposed 2 " "\
- ]
- set newargs [tcl::dict::merge $defaults $argsflags]
- tailcall overtype::renderline {*}$newargs $under $over
-}
-#renderline may not make sense as it is in the long run for blocks of text - but is handy in the single-line-handling form anyway.
-# We are trying to handle ansi codes in a block of text which is acting like a mini-terminal in some sense.
-#We can process standard cursor moves such as \b \r - but no way to respond to other cursor movements e.g moving to other lines.
-#
-tcl::namespace::eval overtype::piper {
- proc renderline {args} {
- if {[llength $args] < 2} {
- error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? overtext pipelinedata}
- }
- foreach {over under} [lrange $args end-1 end] break
- set argsflags [lrange $args 0 end-2]
- tailcall overtype::renderline {*}$argsflags $under $over
- }
-}
-interp alias "" piper_renderline "" overtype::piper::renderline
-
-#intended primarily for single grapheme - but will work for multiple
-#WARNING: query CAN contain ansi or newlines - but if cache was not already set manually,the answer will be incorrect!
-#We deliberately allow this for PM/SOS attached within a column
-#(a cache of ansifreestring_width calls - as these are quite regex heavy)
-proc overtype::grapheme_width_cached {ch} {
- variable grapheme_widths
- if {[tcl::dict::exists $grapheme_widths $ch]} {
- return [tcl::dict::get $grapheme_widths $ch]
- }
- set width [punk::char::ansifreestring_width $ch]
- tcl::dict::set grapheme_widths $ch $width
- return $width
-}
-
-
-
-proc overtype::test_renderline {} {
- set t \uFF5E ;#2-wide tilde
- set u \uFF3F ;#2-wide underscore
- set missing \uFFFD
- return [list $t $u A${t}B]
-}
-
-#maintenance warning
-#same as textblock::size - but we don't want that circular dependency
-#block width and height can be tricky. e.g \v handled differently on different terminal emulators and can affect both
-proc overtype::blocksize {textblock} {
- if {$textblock eq ""} {
- return [tcl::dict::create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings
- }
- if {[tcl::string::first \t $textblock] >= 0} {
- if {[info exists punk::console::tabwidth]} {
- set tw $::punk::console::tabwidth
- } else {
- set tw 8
- }
- set textblock [textutil::tabify::untabify2 $textblock $tw]
- }
- #ansistrip on entire block in one go rather than line by line - result should be the same - review - make tests
- if {[punk::ansi::ta::detect $textblock]} {
- set textblock [punk::ansi::ansistrip $textblock]
- }
- if {[tcl::string::last \n $textblock] >= 0} {
- set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map {\n {}} $textblock]]}] ;#faster than splitting into single-char list
- set width [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]]
- } else {
- set num_le 0
- set width [punk::char::ansifreestring_width $textblock]
- }
- #our concept of block-height is likely to be different to other line-counting mechanisms
- set height [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le
-
- return [tcl::dict::create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize ]] width height
-}
-
-tcl::namespace::eval overtype::priv {
- variable cache_is_sgr [tcl::dict::create]
-
- #we are likely to be asking the same question of the same ansi codes repeatedly
- #caching the answer saves some regex expense - possibly a few uS to lookup vs under 1uS
- #todo - test if still worthwhile after a large cache is built up. (limit cache size?)
- proc is_sgr {code} {
- variable cache_is_sgr
- if {[tcl::dict::exists $cache_is_sgr $code]} {
- return [tcl::dict::get $cache_is_sgr $code]
- }
- set answer [punk::ansi::codetype::is_sgr $code]
- tcl::dict::set cache_is_sgr $code $answer
- return $answer
- }
- # better named render_to_unapplied?
- proc render_unapplied {overlay_grapheme_control_list gci} {
- upvar idx_over idx_over
- upvar unapplied unapplied
- upvar unapplied_list unapplied_list ;#maintaining as a list allows caller to utilize it without having to re-split
- upvar overstacks overstacks
- upvar overstacks_gx overstacks_gx
- upvar overlay_grapheme_control_stacks og_stacks
-
- #set unapplied [join [lrange $overlay_grapheme_control_list $gci+1 end]]
- set unapplied ""
- set unapplied_list [list]
- #append unapplied [join [lindex $overstacks $idx_over] ""]
- #append unapplied [punk::ansi::codetype::sgr_merge_list {*}[lindex $overstacks $idx_over]]
- set sgr_merged [punk::ansi::codetype::sgr_merge_list {*}[lindex $og_stacks $gci]]
- if {$sgr_merged ne ""} {
- lappend unapplied_list $sgr_merged
- }
- switch -- [lindex $overstacks_gx $idx_over] {
- "gx0_on" {
- lappend unapplied_list "\x1b(0"
- }
- "gx0_off" {
- lappend unapplied_list "\x1b(B"
- }
- }
-
- foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] {
- lassign $gc type item
- #types g other sgr gx0
- if {$type eq "gx0"} {
- if {$item eq "gx0_on"} {
- lappend unapplied_list "\x1b(0"
- } elseif {$item eq "gx0_off"} {
- lappend unapplied_list "\x1b(B"
- }
- } else {
- lappend unapplied_list $item
- }
- }
- set unapplied [join $unapplied_list ""]
- }
-
- #clearer - renders the specific gci forward as unapplied - prefixed with it's merged sgr stack
- proc render_this_unapplied {overlay_grapheme_control_list gci} {
- upvar idx_over idx_over
- upvar unapplied unapplied
- upvar unapplied_list unapplied_list
- upvar overstacks overstacks
- upvar overstacks_gx overstacks_gx
- upvar overlay_grapheme_control_stacks og_stacks
-
- #set unapplied [join [lrange $overlay_grapheme_control_list $gci+1 end]]
- set unapplied ""
- set unapplied_list [list]
-
- set sgr_merged [punk::ansi::codetype::sgr_merge_list {*}[lindex $og_stacks $gci]]
- if {$sgr_merged ne ""} {
- lappend unapplied_list $sgr_merged
- }
- switch -- [lindex $overstacks_gx $idx_over] {
- "gx0_on" {
- lappend unapplied_list "\x1b(0"
- }
- "gx0_off" {
- lappend unapplied_list "\x1b(B"
- }
- }
-
- foreach gc [lrange $overlay_grapheme_control_list $gci end] {
- lassign $gc type item
- #types g other sgr gx0
- if {$type eq "gx0"} {
- if {$item eq "gx0_on"} {
- lappend unapplied_list "\x1b(0"
- } elseif {$item eq "gx0_off"} {
- lappend unapplied_list "\x1b(B"
- }
- } else {
- lappend unapplied_list $item
- }
- }
- set unapplied [join $unapplied_list ""]
- }
- proc render_delchar {i} {
- upvar outcols o
- upvar understacks ustacks
- upvar understacks_gx gxstacks
- set nxt [llength $o]
- if {$i < $nxt} {
- set o [lreplace $o $i $i]
- set ustacks [lreplace $ustacks $i $i]
- set gxstacks [lreplace $gxstacks $i $i]
- } elseif {$i == 0 || $i == $nxt} {
- #nothing to do
- } else {
- puts stderr "render_delchar - attempt to delchar at index $i >= number of outcols $nxt - shouldn't happen"
- }
- }
- proc render_erasechar {i count} {
- upvar outcols o
- upvar understacks ustacks
- upvar understacks_gx gxstacks
- upvar replay_codes_overlay replay
- #ECH clears character attributes from erased character positions
- #ECH accepts 0 or empty parameter, which is equivalent to 1. Caller of render_erasechar should do that mapping and only supply 1 or greater.
- if {![tcl::string::is integer -strict $count] || $count < 1} {
- error "render_erasechar count must be integer >= 1"
- }
- set start $i
- set end [expr {$i + $count -1}]
- #we restrict ECH to current line - as some terminals do - review - is that the only way it's implemented?
- if {$i > [llength $o]-1} {
- return
- }
- if {$end > [llength $o]-1} {
- set end [expr {[llength $o]-1}]
- }
- set num [expr {$end - $start + 1}]
- set o [lreplace $o $start $end {*}[lrepeat $num \u0000]] ;#or space?
- #DECECM ???
- set ustacks [lreplace $ustacks $start $end {*}[lrepeat $num [list $replay]]]
- set gxstacks [lreplace $gxstacks $start $end {*}[lrepeat $num [list]]] ;# ??? review
- return
- }
- proc render_setchar {i c } {
- upvar outcols o
- lset o $i $c
- }
-
- #Initial usecase is for old-terminal hack to add PM-wrapped \b
- #review - can be used for other multibyte sequences that occupy one column?
- #combiners? diacritics?
- proc render_append_to_char {i c} {
- upvar outcols o
- if {$i > [llength $o]-1} {
- error "render_append_to_char cannot append [ansistring VIEW -lf 1 -nul 1 $c] to existing char at index $i while $i >= llength outcols [llength $o]"
- }
- set existing [lindex $o $i]
- if {$existing eq "\0"} {
- lset o $i $c
- } else {
- lset o $i $existing$c
- }
- }
- #is actually addgrapheme?
- proc render_addchar {i c sgrstack gx0stack {insert_mode 0}} {
- upvar outcols o
- upvar understacks ustacks
- upvar understacks_gx gxstacks
-
- # -- --- ---
- #this is somewhat of a hack.. probably not really the equivalent of proper reverse video? review
- #we should ideally be able to reverse the video of a sequence that already includes SGR reverse/noreverse attributes
- upvar reverse_mode do_reverse
- #if {$do_reverse} {
- # lappend sgrstack [a+ reverse]
- #} else {
- # lappend sgrstack [a+ noreverse]
- #}
-
- #JMN3
- if {$do_reverse} {
- #note we can't just look for \x1b\[7m or \x1b\[27m
- # it may be a more complex sequence like \x1b\[0\;\;7\;31m etc
-
- set existing_reverse_state 0
- set codeinfo [punk::ansi::codetype::sgr_merge $sgrstack -info 1]
- set codestate_reverse [dict get $codeinfo codestate reverse]
- switch -- $codestate_reverse {
- 7 {
- set existing_reverse_state 1
- }
- 27 {
- set existing_reverse_state 0
- }
- "" {
- }
- }
- if {$existing_reverse_state == 0} {
- set rflip [a+ reverse]
- } else {
- #reverse of reverse
- set rflip [a+ noreverse]
- }
- #note that mergeresult can have multiple esc (due to unmergeables or non sgr codes)
- set sgrstack [list [dict get $codeinfo mergeresult] $rflip]
- #set sgrstack [punk::ansi::codetype::sgr_merge [list [dict get $codeinfo mergeresult] $rflip]]
- }
-
- # -- --- ---
-
- set nxt [llength $o]
- if {!$insert_mode} {
- if {$i < $nxt} {
- #These lists must always be in sync
- lset o $i $c
- } else {
- lappend o $c
- }
- if {$i < [llength $ustacks]} {
- lset ustacks $i $sgrstack
- lset gxstacks $i $gx0stack
- } else {
- lappend ustacks $sgrstack
- lappend gxstacks $gx0stack
- }
- } else {
- #insert of single-width vs double-width when underlying is double-width?
- if {$i < $nxt} {
- set o [linsert $o $i $c]
- } else {
- lappend o $c
- }
- if {$i < [llength $ustacks]} {
- set ustacks [linsert $ustacks $i $sgrstack]
- set gxstacks [linsert $gxstacks $i $gx0stack]
- } else {
- lappend ustacks $sgrstack
- lappend gxstacks $gx0stack
- }
- }
- }
-
-}
-
-
-
-# -- --- --- --- --- --- --- --- --- --- ---
-tcl::namespace::eval overtype {
- interp alias {} ::overtype::center {} ::overtype::centre
-}
-
-# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
-## Ready
-package provide overtype [tcl::namespace::eval overtype {
- variable version
- set version 1.6.6
-}]
-return
-
-#*** !doctools
-#[manpage_end]
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/pattern-1.2.4.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/pattern-1.2.4.tm
deleted file mode 100644
index d6a9c932..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/pattern-1.2.4.tm
+++ /dev/null
@@ -1,1285 +0,0 @@
-#PATTERN
-# - A prototype-based Object system.
-#
-# Julian Noble 2003
-# License: Public domain
-#
-
-# "I need pattern" - Lexx Series 1 Episode 3 - Eating Pattern.
-#
-#
-# Pattern uses a mixture of class-based and prototype-based object instantiation.
-#
-# A pattern object has 'properties' and 'methods'
-# The system makes a distinction between them with regards to the access syntax for write operations,
-# and yet provides unity in access syntax for read operations.
-# e.g >object . myProperty
-# will return the value of the property 'myProperty'
-# >ojbect . myMethod
-# will return the result of the method 'myMethod'
-# contrast this with the write operations:
-# set [>object . myProperty .] blah
-# >object . myMethod blah
-# however, the property can also be read using:
-# set [>object . myProperty .]
-# Note the trailing . to give us a sort of 'reference' to the property.
-# this is NOT equivalent to
-# set [>object . myProperty]
-# This last example is of course calling set against a standard variable whose name is whatever value is returned by reading the property
-# i.e it is equivalent in this case to: set blah
-
-#All objects are represented by a command, the name of which contains a leading ">".
-#Any commands in the interp which use this naming convention are assumed to be a pattern object.
-#Use of non-pattern commands containing this leading character is not supported. (Behaviour is undefined)
-
-#All user-added properties & methods of the wrapped object are accessed
-# using the separator character "."
-#Metamethods supplied by the patterm system are accessed with the object command using the metamethod separator ".."
-# e.g to instantiate a new object from an existing 'pattern' (the equivalent of a class or prototype)
-# you would use the 'Create' metamethod on the pattern object like so:
-# >MyFactoryClassOrPrototypeLikeThing .. Create >NameOfNewObject
-# '>NameOfNewObject' is now available as a command, with certain inherited methods and properties
-# of the object it was created from. (
-
-
-#The use of the access-syntax separator character "." allows objects to be kept
-# 'clean' in the sense that the only methods &/or properties that can be called this way are ones
-# the programmer(you!) put there. Existing metamethods such as 'Create' are accessed using a different syntax
-# so you are free to implement your own 'Create' method on your object that doesn't conflict with
-# the metamethod.
-
-#Chainability (or how to violate the Law of Demeter!)
-#The . access-syntax gives TCL an OO syntax more closely in line with many OO systems in other
-# languages such as Python & VB, and allows left to right keyboard-entry of a deeply nested object-reference
-# structure, without the need to regress to enter matching brackets as is required when using
-# standard TCL command syntax.
-# ie instead of:
-# [[[object nextObject] getItem 4] getItem [chooseItemNumber]] doSomething
-# we can use:
-# >object . nextObject . getItem 4 . getItem [chooseItemNumber] . doSomething
-#
-# This separates out the object-traversal syntax from the TCL command syntax.
-
-# . is the 'traversal operator' when it appears between items in a commandlist
-# . is the 'reference operator' when it is the last item in a commandlist
-# , is the 'index traversal operator' (or 'nest operator') - mathematically it marks where there is a matrix 'partition'.
-# It marks breaks in the multidimensional structure that correspond to how the data is stored.
-# e.g obj . arraydata x y , x1 y1 z1
-# represents an element of a 5-dimensional array structured as a plane of cubes
-# e.g2 obj . arraydata x y z , x1 y1
-# represents an element of a 5-dimensional array structured as a cube of planes
-# The underlying storage for e.g2 might consist of something such as a Tcl array indexed such as cube($x,$y,$z) where each value is a patternlib::>matrix object with indices x1 y1
-# .. is the 'meta-traversal operator' when it appears between items in a commandlist
-# .. is the 'meta-info operator'(?) when it is the last item in a commandlist
-
-
-#!todo - Duck Typing: http://en.wikipedia.org/wiki/Duck_typing
-# implement iStacks & pStacks (interface stacks & pattern stacks)
-
-#see also: Using namsepace ensemble without a namespace: http://wiki.tcl.tk/16975
-
-
-#------------------------------------------------------------
-# System objects.
-#------------------------------------------------------------
-#::p::-1 ::p::internals::>metaface
-#::p::0 ::p::ifaces::>null
-#::p::1 ::>pattern
-#------------------------------------------------------------
-
-#TODO
-
-#investigate use of [namespace path ... ] to resolve command lookup (use it to chain iStacks?)
-
-
-#CHANGES
-#2018-09 - v 1.2.2
-# varied refactoring
-# Changed invocant datastructure curried into commands (the _ID_ structure)
-# Changed MAP structure to dict
-# Default Method no longer magic "item" - must be explicitly set with .. DefaultMethod (or .. PatternDefaultMethod for patterns)
-# updated test suites
-#2018-08 - v 1.2.1
-# split ::p::predatorX functions into separate files (pkgs)
-# e.g patternpredator2-1.0.tm
-# patternpredator1-1.0 - split out but not updated/tested - probably obsolete and very broken
-#
-#2017-08 - v 1.1.6 Fairly big overhaul
-# New predator function using coroutines
-# Added bang operator !
-# Fixed Constructor chaining
-# Added a few tests to test::pattern
-#
-#2008-03 - preserve ::errorInfo during var writes
-
-#2007-11
-#Major overhaul + new functionality + new tests v 1.1
-# new dispatch system - 'predator'.
-# (preparing for multiple interface stacks, multiple invocants etc)
-#
-#
-#2006-05
-# Adjusted 'var' expansion to use the new tcl8.5 'namespace upvar $ns v1 n1 v2 n2 ... ' feature.
-#
-#2005-12
-# Adjusted 'var' expansion in method/constructor etc bodies to be done 'inline' where it appears rather than aggregated at top.
-#
-# Fixed so that PatternVariable default applied on Create.
-#
-# unified interface/object datastructures under ::p:::: instead of seperate ::p::IFACE::::
-# - heading towards multiple-interface objects
-#
-#2005-10-28
-# 1.0.8.1 passes 80/80 tests
-# >object .. Destroy - improved cleanup of interfaces & namespaces.
-#
-#2005-10-26
-# fixes to refsync (still messy!)
-# remove variable traces on REF vars during .. Destroy
-# passes 76/76
-#
-#2005-10-24
-# fix objectRef_TraceHandler so that reading a property via an object reference using array syntax will call a PropertyRead function if defined.
-# 1.0.8.0 now passes 75/76
-#
-#2005-10-19
-# Command alias introduced by @next@ is now placed in the interfaces namespace. (was unnamespaced before)
-# changed IFACE array names for level0 methods to be m-1 instead of just m. (now consistent with higher level m-X names)
-# 1.0.8.0 (passes 74/76)
-# tests now in own package
-# usage:
-# package require test::pattern
-# test::p::list
-# test::p::run ?nameglob? ?-version ?
-#
-#2005-09?-12
-#
-# fixed standalone 'var' statement in method bodies so that no implicit variable declarations added to proc.
-# fixed @next@ so that destination method resolved at interface compile time instead of call time
-# fixed @next@ so that on Create, .. PatternMethod x overlays existing method produced by a previous .. PatternMethod x.
-# (before, the overlay only occured when '.. Method' was used to override.)
-#
-#
-# miscellaneous tidy-ups
-#
-# 1.0.7.8 (passes 71/73)
-#
-#2005-09-10
-# fix 'unknown' system such that unspecified 'unknown' handler represented by lack of (unknown) variable instead of empty string value
-# this is so that a mixin with an unspecified 'unknown' handler will not undo a lowerlevel 'unknown' specificier.
-#
-#2005-09-07
-# bugfix indexed write to list property
-# bugfix Variable default value
-# 1.0.7.7 (passes 70/72)
-# fails:
-# arrayproperty.test - array-entire-reference
-# properties.test - property_getter_filter_via_ObjectRef
-#
-#2005-04-22
-# basic fix to PatternPropertyRead dispatch code - updated tests (indexed case still not fixed!)
-#
-# 1.0.7.4
-#
-#2004-11-05
-# basic PropertyRead implementation (non-indexed - no tests!)
-#
-#2004-08-22
-# object creation speedups - (pattern::internals::obj simplified/indirected)
-#
-#2004-08-17
-# indexed property setter fixes + tests
-# meta::Create fixes - state preservation on overlay (correct constructor called, property defaults respect existing values)
-#
-#2004-08-16
-# PropertyUnset & PatternPropertyUnset metaMethods (filter method called on property unset)
-#
-#2004-08-15
-# reference syncing: ensure writes to properties always trigger traces on property references (+ tests)
-# - i.e method that updates o_myProp var in >myObj will cause traces on [>myObj . myProp .] to trigger
-# - also trigger on curried traces to indexed properties i.e list and array elements.
-# - This feature presumably adds some overhead to all property writes - !todo - investigate desirability of mechanism to disable on specific properties.
-#
-# fix (+ tests) for ref to multiple indices on object i.e [>myObj key1 key2 .]
-#
-#2004-08-05
-# add PropertyWrite & PatternPropertyWrite metaMethods - (filter method called on property write)
-#
-# fix + add tests to support method & property of same name. (method precedence)
-#
-#2004-08-04
-# disallow attempt to use method reference as if it were a property (raise error instead of silently setting useless var)
-#
-# 1.0.7.1
-# use objectref array access to read properties even when some props unset; + test
-# unset property using array access on object reference; + test
-#
-#
-#2004-07-21
-# object reference changes - array property values appear as list value when accessed using upvared array.
-# bugfixes + tests - properties containing lists (multidimensional access)
-#
-#1.0.7
-#
-#2004-07-20
-# fix default property value append problem
-#
-#2004-07-17
-# add initial implementation of 'Unknown' and 'PatternUnknown' meta-methods
-# (
-#
-#2004-06-18
-# better cleanup on '>obj .. Destroy' - recursively destroy objects under parents subnamespaces.
-#
-#2004-06-05
-# change argsafety operator to be anything with leading -
-# if standalone '-' then the dash itself is not added as a parameter, but if a string follows '-'
-# i.e tkoption style; e.g -myoption ; then in addition to acting as an argsafety operator for the following arg,
-# the entire dash-prefixed operator is also passed in as an argument.
-# e.g >object . doStuff -window .
-# will call the doStuff method with the 2 parameters -window .
-# >object . doStuff - .
-# will call doStuff with single parameter .
-# >object . doStuff - -window .
-# will result in a reference to the doStuff method with the argument -window 'curried' in.
-#
-#2004-05-19
-#1.0.6
-# fix so custom constructor code called.
-# update Destroy metamethod to unset $self
-#
-#1.0.4 - 2004-04-22
-# bug fixes regarding method specialisation - added test
-#
-#------------------------------------------------------------
-
-package provide pattern [namespace eval pattern {variable version; set version 1.2.4}]
-
-
-namespace eval pattern::util {
-
- # Generally better to use 'package require $minver-'
- # - this only gives us a different error
- proc package_require_min {pkg minver} {
- if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} {
- package require $pkg
- } else {
- error "Package pattern requires package $pkg of at least version $minver. Available: $available"
- }
- }
-}
-
-package require patterncmd 1.2.4-
-package require metaface 1.2.4- ;#utility/system diagnostic commands (may be used by metaface lib etc)
-
-
-
-#package require cmdline
-package require overtype
-
-#package require md5 ;#will be loaded if/when needed
-#package require md4
-#package require uuid
-
-
-
-
-
-namespace eval pattern {
- variable initialised 0
-
-
- if 0 {
- if {![catch {package require twapi_base} ]} {
- #twapi is a windows only package
- #MUCH faster to load just twapi_base than full 'package require twapi' IFF using the modular twapi distribution with multiple separately loadable dlls.
- # If available - windows seems to provide a fast uuid generator..
- #*IF* tcllibc is missing, then as at 2008-05 twapi::new_uuid is significantly faster than uuid::uuid generate ( e.g 19 usec vs 76thousand usec! on 2.4GHZ machine)
- # (2018 update - 15-30usec vs ~200usec on core i9 @ ~2.6GHZ (time for a single call e.g time {pattern::new_uuid}))
- interp alias {} ::pattern::new_uuid {} ::twapi::new_uuid -localok
- } else {
- #performance on freebsd seems not great, but adequate. (e.g 500usec on dualcore 1.6GHZ)
- # (e.g 200usec 2018 corei9)
- #(with or without tcllibc?)
- #very first call is extremely slow though - 3.5seconds on 2018 corei9
- package require uuid
- interp alias {} ::pattern::new_uuid {} ::uuid::uuid generate
- }
- #variable fastobj 0 ;#precalculated invocant ID in method body (instead of call time ) - removed for now - see pattern 1.2.1 (a premature optimisation which was hampering refactoring & advancement)
- }
-
-
-}
-
-
-
-
-
-
-namespace eval p {
- #this is also the interp alias namespace. (object commands created here , then renamed into place)
- #the object aliases are named as incrementing integers.. !todo - consider uuids?
- variable ID 0
- namespace eval internals {}
-
-
- #!??
- #namespace export ??
- variable coroutine_instance 0
-}
-
-#-------------------------------------------------------------------------------------
-#review - what are these for?
-#note - this function is deliberately not namespaced
-# - it begins with the letters 'proc' (as do the created aliases) - to aid in editor's auto indexing/mapping features
-proc process_pattern_aliases {object args} {
- set o [namespace tail $object]
- interp alias {} process_patternmethod_$o {} [$object .. PatternMethod .]
- interp alias {} process_method_$o {} [$object .. Method .]
- interp alias {} process_constructor_$o {} [$object .. Constructor .]
-}
-#-------------------------------------------------------------------------------------
-
-
-
-
-#!store all interface objects here?
-namespace eval ::p::ifaces {}
-
-
-
-#K combinator - see http://wiki.tcl.tk/1923
-#proc ::p::K {x y} {set x}
-#- not used - use inline K if desired i.e set x [lreplace $x[set x{}] $a $b blah]
-
-
-
-
-
-
-
-
-proc ::p::internals::(VIOLATE) {_ID_ violation_script} {
- #set out [::p::fixed_var_statements @IMPLICITDECLS@\n$violation_script]
- set processed [dict create {*}[::p::predator::expand_var_statements $violation_script]]
-
- if {![dict get $processed explicitvars]} {
- #no explicit var statements - we need the implicit ones
- set self [set ::p::${_ID_}::(self)]
- set IFID [lindex [set $self] 1 0 end]
- #upvar ::p::${IFID}:: self_IFINFO
-
-
- set varDecls {}
- set vlist [array get ::p::${IFID}:: v,name,*]
- set _k ""; set v ""
- if {[llength $vlist]} {
- append varDecls "upvar #0 "
- foreach {_k v} $vlist {
- append varDecls "::p::\${_ID_}::$v $v "
- }
- append varDecls "\n"
- }
-
- #set violation_script [string map [::list @IMPLICITDECLS@ $varDecls] $out]
- set violation_script $varDecls\n[dict get $processed body]
-
- #tidy up
- unset processed varDecls self IFID _k v
- } else {
- set violation_script [dict get $processed body]
- }
- unset processed
-
-
-
-
- #!todo - review (& document) exactly what context this script runs in and what vars/procs are/should be visible.
- eval "unset violation_script;$violation_script"
-}
-
-
-proc ::p::internals::DestroyObjectsBelowNamespace {ns} {
- #puts "\n##################\n#################### destroyObjectsBelowNamespace $ns\n"
-
- set nsparts [split [string trim [string map {:: :} $ns] :] :]
- if { ! ( ([llength $nsparts] == 3) & ([lindex $nsparts 0] == "p") & ([lindex $nsparts end] eq "_ref") )} {
- #ns not of form ::p::?::_ref
-
- foreach obj [info commands ${ns}::>*] {
- #catch {::p::meta::Destroy $obj}
- #puts ">>found object $obj below ns $ns - destroying $obj"
- $obj .. Destroy
- }
- }
-
- #set traces [trace info variable ${ns}::-->PATTERN_ANCHOR]
- #foreach tinfo $traces {
- # trace remove variable ${ns}::-->PATTERN_ANCHOR {*}$tinfo
- #}
- #unset -nocomplain ${ns}::-->PATTERN_ANCHOR
-
- foreach sub [namespace children $ns] {
- ::p::internals::DestroyObjectsBelowNamespace $sub
- }
-}
-
-
-
-
-#################################################
-#################################################
-#################################################
-#################################################
-#################################################
-#################################################
-#################################################
-#################################################
-#################################################
-#################################################
-
-
-
-
-
-
-
-
-
-proc ::p::get_new_object_id {} {
- tailcall incr ::p::ID
- #tailcall ::pattern::new_uuid
-}
-
-#create a new minimal object - with no interfaces or patterns.
-
-#proc ::p::internals::new_object [list cmd {wrapped ""} [list OID [expr {-2}]]] {}
-proc ::p::internals::new_object {cmd {wrapped ""} {OID "-2"}} {
-
- #puts "-->new_object cmd:$cmd wrapped:$wrapped OID:$OID"
-
- if {$OID eq "-2"} {
- set OID [::p::get_new_object_id]
- #set OID [incr ::p::ID] ;#!todo - use uuids? (too slow?) (use uuids as configurable option?, pre-allocate a list of uuids?)
- #set OID [pattern::new_uuid]
- }
- #if $wrapped provided it is assumed to be an existing namespace.
- #if {[string length $wrapped]} {
- # #???
- #}
-
- #sanity check - alias must not exist for this OID
- if {[llength [interp alias {} ::p::$OID]]} {
- error "Object alias '::p::$OID' already exists - cannot create new object with this id"
- }
-
- #system 'varspaces' -
-
- #until we have a version of Tcl that doesn't have 'creative writing' scope issues -
- # - we should either explicity specify the whole namespace when setting variables or make sure we use the 'variable' keyword.
- # (see http://wiki.tcl.tk/1030 'Dangers of creative writing')
- #set o_open 1 - every object is initially also an open interface (?)
- #NOTE! comments within namespace eval slow it down.
- namespace eval ::p::$OID {
- #namespace ensemble create
- namespace eval _ref {}
- namespace eval _meta {}
- namespace eval _iface {
- variable o_usedby;
- variable o_open 1;
- array set o_usedby [list];
- variable o_varspace "" ;
- variable o_varspaces [list];
- variable o_methods [dict create];
- variable o_properties [dict create];
- variable o_variables;
- variable o_propertyunset_handlers;
- set o_propertyunset_handlers [dict create]
- }
- }
-
- #set alias ::p::$OID
-
- #objectid alis default_method object_command wrapped_namespace
- set INVOCANTDATA [list $OID ::p::$OID "" $cmd $wrapped]
-
- #MAP is a dict
- set MAP [list invocantdata $INVOCANTDATA interfaces {level0 {} level0_default "" level1 {} level1_default ""} patterndata {patterndefaultmethod ""}]
-
-
-
- #NOTE 'interp alias' will prepend :: if chosen srccmd already exists as an alias token
- #we've already checked that ::p::$OID doesn't pre-exist
- # - so we know the return value of the [interp alias {} $alias {} ...] will be $alias
- #interp alias {} ::p::$OID {} ::p::internals::predator $MAP
-
-
- # _ID_ structure
- set invocants_dict [dict create this [list $INVOCANTDATA] ]
- #puts stdout "New _ID_structure: $interfaces_dict"
- set _ID_ [dict create i $invocants_dict context ""]
-
-
- interp alias {} ::p::$OID {} ::p::internals::predator $_ID_
- #rename the command into place - thus the alias & the command name no longer match!
- rename ::p::$OID $cmd
-
- set ::p::${OID}::_meta::map $MAP
-
- # called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something
- interp alias {} ::p::${OID}:: {} ::p::internals::no_default_method $_ID_
-
- #set p2 [string map {> ?} $cmd]
- #interp alias {} $p2 {} ::p::internals::alternative_predator $_ID_
-
-
- #trace add command $cmd delete "$cmd .. Destroy ;#"
- #puts "@@@ trace add command $cmd rename [list $cmd .. Rename]"
-
- trace add command $cmd rename [list $cmd .. Rename] ;#will receive $oldname $newname "rename"
- #trace add command $cmd rename [$cmd .. Rename .] ;#EXTREMELY slow. (but why?)
-
- #puts "@@@ trace added for $cmd -> '[trace info command $cmd]'"
-
-
- #uplevel #0 "trace add command $cmd delete \"puts deleting$cmd ;#\""
- #trace add command $cmd delete "puts deleting$cmd ;#"
- #puts stdout "trace add command $cmd delete \"puts deleting$cmd ;#\""
-
-
- #puts "--> new_object returning map $MAP"
- return $MAP
-}
-
-
-
-
-#>x .. Create >y
-# ".." is special case equivalent to "._."
-# (whereas in theory it would be ".default.")
-# "." is equivalent to ".default." is equivalent to ".default.default." (...)
-
-#>x ._. Create >y
-#>x ._.default. Create >y ???
-#
-#
-
-# create object using 'blah' as source interface-stack ?
-#>x .blah. .. Create >y
-#>x .blah,_. ._. Create .iStackDestination. >y
-
-
-
-#
-# ">x .blah,_." is a reference(cast) to >x that contains only the iStacks in the order listed. i.e [list blah _]
-# the 1st item, blah in this case becomes the 'default' iStack.
-#
-#>x .*.
-# cast to object with all iStacks
-#
-#>x .*,!_.
-# cast to object with all iStacks except _
-#
-# ---------------------
-#!todo - MultiMethod support via transient and persistent object conglomerations. Operators '&' & '@'
-# - a persistent conglomeration will have an object id (OID) and thus associated namespace, whereas a transient one will not.
-#
-#eg1: >x & >y . some_multi_method arg arg
-# this is a call to the MultiMethod 'some_multi_method' with 2 objects as the invocants. ('>x & >y' is a transient conglomeration of the two objects)
-# No explicit 'invocation role' is specified in this call - so it gets the default role for multiple invocants: 'these'
-# The invocant signature is thus {these 2}
-# (the default invocation role for a standard call on a method with a single object is 'this' - with the associated signature {this 1})
-# Invocation roles can be specified in the call using the @ operator.
-# e.g >x & >y @ points . some_multi_method arg arg
-# The invocant signature for this is: {points 2}
-#
-#eg2: {*}[join $objects &] @ objects & >p @ plane . move $path
-# This has the signature {objects n plane 1} where n depends on the length of the list $objects
-#
-#
-# To get a persistent conglomeration we would need to get a 'reference' to the conglomeration.
-# e.g set pointset [>x & >y .]
-# We can now call multimethods on $pointset
-#
-
-
-
-
-
-
-#set ::p::internals::predator to a particular predatorversion (from a patternpredatorX package)
-proc ::pattern::predatorversion {{ver ""}} {
- variable active_predatorversion
- set allowed_predatorversions {1 2}
- set default_predatorversion [lindex $allowed_predatorversions end] ;#default to last in list of allowed_predatorversions
-
- if {![info exists active_predatorversion]} {
- set first_time_set 1
- } else {
- set first_time_set 0
- }
-
- if {$ver eq ""} {
- #get version
- if {$first_time_set} {
- set active_predatorversions $default_predatorversion
- }
- return $active_predatorversion
- } else {
- #set version
- if {$ver ni $allowed_predatorversions} {
- error "Invalid attempt to set predatorversion - unknown value: $ver, try one of: $allowed_predatorversions"
- }
-
- if {!$first_time_set} {
- if {$active_predatorversion eq $ver} {
- #puts stderr "Active predator version is already '$ver'"
- #ok - nothing to do
- return $active_predatorversion
- } else {
- package require patternpredator$ver 1.2.4-
- if {![llength [info commands ::p::predator$ver]]} {
- error "Unable to set predatorversion - command ::p::predator$ver not found"
- }
- rename ::p::internals::predator ::p::predator$active_predatorversion
- }
- }
- package require patternpredator$ver 1.2.4-
- if {![llength [info commands ::p::predator$ver]]} {
- error "Unable to set predatorversion - command ::p::predator$ver not found"
- }
-
- rename ::p::predator$ver ::p::internals::predator
- set active_predatorversion $ver
-
- return $active_predatorversion
- }
-}
-::pattern::predatorversion 2
-
-
-
-
-
-
-
-
-
-
-
-
-# >pattern has object ID 1
-# meta interface has object ID 0
-proc ::pattern::init args {
-
- if {[set ::pattern::initialised]} {
- if {[llength $args]} {
- #if callers want to avoid this error, they can do their own check of $::pattern::initialised
- error "pattern package is already initialised. Unable to apply args: $args"
- } else {
- return 1
- }
- }
-
- #this seems out of date.
- # - where is PatternPropertyRead?
- # - Object is obsolete
- # - Coinjoin, Combine don't seem to exist
- array set ::p::metaMethods {
- Clone object
- Conjoin object
- Combine object
- Create object
- Destroy simple
- Info simple
- Object simple
- PatternProperty simple
- PatternPropertyWrite simple
- PatternPropertyUnset simple
- Property simple
- PropertyWrite simple
- PatternMethod simple
- Method simple
- PatternVariable simple
- Variable simple
- Digest simple
- PatternUnknown simple
- Unknown simple
- }
- array set ::p::metaProperties {
- Properties object
- Methods object
- PatternProperties object
- PatternMethods object
- }
-
-
-
-
-
- #create metaface - IID = -1 - also OID = -1
- # all objects implement this special interface - accessed via the .. operator.
-
-
-
-
-
- set ::p::ID 4 ;#0,1,2,3 reserved for null interface,>pattern, >ifinfo & ::p::>interface
-
-
- #OID = 0
- ::p::internals::new_object ::p::ifaces::>null "" 0
-
- #? null object has itself as level0 & level1 interfaces?
- #set ::p::ifaces::>null [list [list 0 ::p::ifaces::>null item] [list [list 0] [list 0]] [list {} {}]]
-
- #null interface should always have 'usedby' members. It should never be extended.
- array set ::p::0::_iface::o_usedby [list i-1 ::p::internals::>metaface i0 ::p::ifaces::>null i1 ::>pattern] ;#'usedby' array
- set ::p::0::_iface::o_open 0
-
- set ::p::0::_iface::o_constructor [list]
- set ::p::0::_iface::o_variables [list]
- set ::p::0::_iface::o_properties [dict create]
- set ::p::0::_iface::o_methods [dict create]
- set ::p::0::_iface::o_varspace ""
- set ::p::0::_iface::o_varspaces [list]
- array set ::p::0::_iface::o_definition [list]
- set ::p::0::_iface::o_propertyunset_handlers [dict create]
-
-
-
-
- ###############################
- # OID = 1
- # >pattern
- ###############################
- ::p::internals::new_object ::>pattern "" 1
-
- #set ::>pattern [list [list 1 ::>pattern item] [list [list 0] [list 0]]]
-
-
- array set ::p::1::_iface::o_usedby [list] ;#'usedby' array
-
- set _self ::pattern
-
- #set IFID [::p::internals::new_interface 1] ;#level 0 interface usedby object 1
- #set IFID_1 [::p::internals::new_interface 1] ;#level 1 interface usedby object 1
-
-
-
- #1)this object references its interfaces
- #lappend ID $IFID $IFID_1
- #lset SELFMAP 1 0 $IFID
- #lset SELFMAP 2 0 $IFID_1
-
-
- #set body [string map [::list @self@ ::>pattern @_self@ ::pattern @self_ID@ 0 @itemCmd@ item] $::p::internals::OBJECTCOMMAND]
- #proc ::>pattern args $body
-
-
-
-
- #######################################################################################
- #OID = 2
- # >ifinfo interface for accessing interfaces.
- #
- ::p::internals::new_object ::p::ifaces::>2 "" 2 ;#>ifinfo object
- set ::p::2::_iface::o_constructor [list]
- set ::p::2::_iface::o_variables [list]
- set ::p::2::_iface::o_properties [dict create]
- set ::p::2::_iface::o_methods [dict create]
- set ::p::2::_iface::o_varspace ""
- set ::p::2::_iface::o_varspaces [list]
- array set ::p::2::_iface::o_definition [list]
- set ::p::2::_iface::o_open 1 ;#open for extending
-
- ::p::ifaces::>2 .. AddInterface 2
-
- #Manually create a minimal >ifinfo implementation using the same general pattern we use for all method implementations
- #(bootstrap because we can't yet use metaface methods on it)
-
-
-
- proc ::p::2::_iface::isOpen.1 {_ID_} {
- return $::p::2::_iface::o_open
- }
- interp alias {} ::p::2::_iface::isOpen {} ::p::2::_iface::isOpen.1
-
- proc ::p::2::_iface::isClosed.1 {_ID_} {
- return [expr {!$::p::2::_iface::o_open}]
- }
- interp alias {} ::p::2::_iface::isClosed {} ::p::2::_iface::isClosed.1
-
- proc ::p::2::_iface::open.1 {_ID_} {
- set ::p::2::_iface::o_open 1
- }
- interp alias {} ::p::2::_iface::open {} ::p::2::_iface::open.1
-
- proc ::p::2::_iface::close.1 {_ID_} {
- set ::p::2::_iface::o_open 0
- }
- interp alias {} ::p::2::_iface::close {} ::p::2::_iface::close.1
-
-
- #proc ::p::2::_iface::(GET)properties.1 {_ID_} {
- # set ::p::2::_iface::o_properties
- #}
- #interp alias {} ::p::2::_iface::(GET)properties {} ::p::2::_iface::(GET)properties.1
-
- #interp alias {} ::p::2::properties {} ::p::2::_iface::(GET)properties
-
-
- #proc ::p::2::_iface::(GET)methods.1 {_ID_} {
- # set ::p::2::_iface::o_methods
- #}
- #interp alias {} ::p::2::_iface::(GET)methods {} ::p::2::_iface::(GET)methods.1
- #interp alias {} ::p::2::methods {} ::p::2::_iface::(GET)methods
-
-
-
-
-
- #link from object to interface (which in this case are one and the same)
-
- #interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen [::p::ifaces::>2 --]
- #interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed [::p::ifaces::>2 --]
- #interp alias {} ::p::2::open {} ::p::2::_iface::open [::p::ifaces::>2 --]
- #interp alias {} ::p::2::close {} ::p::2::_iface::close [::p::ifaces::>2 --]
-
- interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen
- interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed
- interp alias {} ::p::2::open {} ::p::2::_iface::open
- interp alias {} ::p::2::close {} ::p::2::_iface::close
-
-
- #namespace eval ::p::2 "namespace export $method"
-
- #######################################################################################
-
-
-
-
-
-
- set ::pattern::initialised 1
-
-
- ::p::internals::new_object ::p::>interface "" 3
- #create a convenience object on which to manipulate the >ifinfo interface
- #set IF [::>pattern .. Create ::p::>interface]
- set IF ::p::>interface
-
-
- #!todo - put >ifinfo on a separate pStack so that end-user can more freely treat interfaces as objects?
- # (or is forcing end user to add their own pStack/iStack ok .. ?)
- #
- ::p::>interface .. AddPatternInterface 2 ;#
-
- ::p::>interface .. PatternVarspace _iface
-
- ::p::>interface .. PatternProperty methods
- ::p::>interface .. PatternPropertyRead methods {} {
- varspace _iface
- var {o_methods alias}
- return $alias
- }
- ::p::>interface .. PatternProperty properties
- ::p::>interface .. PatternPropertyRead properties {} {
- varspace _iface
- var o_properties
- return $o_properties
- }
- ::p::>interface .. PatternProperty variables
-
- ::p::>interface .. PatternProperty varspaces
-
- ::p::>interface .. PatternProperty definition
-
- ::p::>interface .. Constructor {{usedbylist {}}} {
- #var this
- #set this @this@
- #set ns [$this .. Namespace]
- #puts "-> creating ns ${ns}::_iface"
- #namespace eval ${ns}::_iface {}
-
- varspace _iface
- var o_constructor o_variables o_properties o_methods o_definition o_usedby o_varspace o_varspaces
-
- set o_constructor [list]
- set o_variables [list]
- set o_properties [dict create]
- set o_methods [dict create]
- set o_varspaces [list]
- array set o_definition [list]
-
- foreach usedby $usedbylist {
- set o_usedby(i$usedby) 1
- }
-
-
- }
- ::p::>interface .. PatternMethod isOpen {} {
- varspace _iface
- var o_open
-
- return $o_open
- }
- ::p::>interface .. PatternMethod isClosed {} {
- varspace _iface
- var o_open
-
- return [expr {!$o_open}]
- }
- ::p::>interface .. PatternMethod open {} {
- varspace _iface
- var o_open
- set o_open 1
- }
- ::p::>interface .. PatternMethod close {} {
- varspace _iface
- var o_open
- set o_open 0
- }
- ::p::>interface .. PatternMethod refCount {} {
- varspace _iface
- var o_usedby
- return [array size o_usedby]
- }
-
- set ::p::2::_iface::o_open 1
-
-
-
-
- uplevel #0 {pattern::util::package_require_min patternlib 1.2.4}
- #uplevel #0 {package require patternlib}
- return 1
-}
-
-
-
-proc ::p::merge_interface {old new} {
- #puts stderr " ** ** ** merge_interface $old $new"
- set ns_old ::p::$old
- set ns_new ::p::$new
-
- upvar #0 ::p::${new}:: IFACE
- upvar #0 ::p::${old}:: IFACEX
-
- if {![catch {set c_arglist $IFACEX(c,args)}]} {
- #constructor
- #for now.. just add newer constructor regardless of any existing one
- #set IFACE(c,args) $IFACEX(c,args)
-
- #if {![info exists IFACE(c,args)]} {
- # #target interface didn't have a constructor
- #
- #} else {
- # #
- #}
- }
-
-
- set methods [::list]
- foreach nm [array names IFACEX m-1,name,*] {
- lappend methods [lindex [split $nm ,] end] ;#use the method key-name not the value! (may have been overridden)
- }
-
- #puts " *** merge interface $old -> $new ****merging-in methods: $methods "
-
- foreach method $methods {
- if {![info exists IFACE(m-1,name,$method)]} {
- #target interface doesn't yet have this method
-
- set THISNAME $method
-
- if {![string length [info command ${ns_new}::$method]]} {
-
- if {![set ::p::${old}::_iface::o_open]} {
- #interp alias {} ${ns_new}::$method {} ${ns_old}::$method
- #namespace eval $ns_new "namespace export [namespace tail $method]"
- } else {
- #wait to compile
- }
-
- } else {
- error "merge interface - command collision "
- }
- #set i 2 ???
- set i 1
-
- } else {
- #!todo - handle how?
- #error "command $cmd already exists in interface $new"
-
-
- set i [incr IFACE(m-1,chain,$method)]
-
- set THISNAME ___system___override_${method}_$i
-
- #move metadata using subindices for delegated methods
- set IFACE(m-$i,name,$method) $IFACE(m-1,name,$method)
- set IFACE(m-$i,iface,$method) $IFACE(m-1,iface,$method)
- set IFACE(mp-$i,$method) $IFACE(mp-1,$method)
-
- set IFACE(m-$i,body,$method) $IFACE(m-1,body,$method)
- set IFACE(m-$i,args,$method) $IFACE(m-1,args,$method)
-
-
- #set next [::p::next_script $IFID0 $method]
- if {![string length [info command ${ns_new}::$THISNAME]]} {
- if {![set ::p::${old}::_iface::o_open]} {
- interp alias {} ${ns_new}::$THISNAME {} ${ns_old}::$method
- namespace eval $ns_new "namespace export $method"
- } else {
- #wait for compile
- }
- } else {
- error "merge_interface - command collision "
- }
-
- }
-
- array set IFACE [::list \
- m-1,chain,$method $i \
- m-1,body,$method $IFACEX(m-1,body,$method) \
- m-1,args,$method $IFACEX(m-1,args,$method) \
- m-1,name,$method $THISNAME \
- m-1,iface,$method $old \
- ]
-
- }
-
-
-
-
-
- #array set ${ns_new}:: [array get ${ns_old}::]
-
-
- #!todo - review
- #copy everything else across..
-
- foreach {nm v} [array get IFACEX] {
- #puts "-.- $nm"
- if {([string first "m-1," $nm] != 0) && ($nm ne "usedby")} {
- set IFACE($nm) $v
- }
- }
-
- #!todo -write a test
- set ::p::${new}::_iface::o_open 1
-
- #!todo - is this done also when iface compiled?
- #namespace eval ::p::$new {namespace ensemble create}
-
-
- #puts stderr "copy_interface $old $new"
-
- #assume that the (usedby) data is now obsolete
- #???why?
- #set ${ns_new}::(usedby) [::list]
-
- #leave ::(usedby) reference in place
-
- return
-}
-
-
-
-
-#detect attempt to treat a reference to a method as a property
-proc ::p::internals::commandrefMisuse_TraceHandler {OID field args} {
-#puts "commandrefMisuse_TraceHandler fired OID:$OID field:$field args:$args"
- lassign [lrange $args end-2 end] vtraced vidx op
- #NOTE! cannot rely on vtraced as it may have been upvared
-
- switch -- $op {
- write {
- error "$field is not a property" "property ref write failure for property $field (OID: $OID refvariable: [lindex $args 0])"
- }
- unset {
- #!todo - monitor stat of Tcl bug# 1911919 - when/(if?) fixed - reinstate 'unset' trace
- #trace add variable $traced {read write unset} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args]
-
- #!todo - don't use vtraced!
- trace add variable $vtraced {read write unset array} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args]
-
- #pointless raising an error as "Any errors in unset traces are ignored"
- #error "cannot unset. $field is a method not a property"
- }
- read {
- error "$field is not a property (args $args)" "property ref read failure for property $field (OID: $OID refvariable: [lindex $args 0])"
- }
- array {
- error "$field is not a property (args $args)" "property ref use as array failure for property $field (OID: $OID refvariable: [lindex $args 0])"
- #error "unhandled operation in commandrefMisuse_TraceHandler - got op:$op expected read,write,unset. OID:$OID field:$field args:$args"
- }
- }
-
- return
-}
-
-
-
-
-#!todo - review calling-points for make_dispatcher.. probably being called unnecessarily at some points.
-#
-# The 'dispatcher' is an object instance's underlying object command.
-#
-
-#proc ::p::make_dispatcher {obj ID IFID} {
-# proc [string map {::> ::} $obj] {{methprop INFO} args} [string map [::list @IID@ $IFID @oid@ $ID] {
-# ::p::@IID@ $methprop @oid@ {*}$args
-# }]
-# return
-#}
-
-
-
-
-################################################################################################################################################
-################################################################################################################################################
-################################################################################################################################################
-
-#aliased from ::p::${OID}::
-# called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something
-proc ::p::internals::no_default_method {_ID_ args} {
- puts stderr "p::internals::no_default_method _ID_:'$_ID_' args:'$args'"
- lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command wrapped
- tailcall error "No default method on object $object_command. (To get or set, use: $object_command .. DefaultMethod ?methodname? or use PatternDefaultMethod)"
-}
-
-#force 1 will extend an interface even if shared. (??? why is this necessary here?)
-#if IID empty string - create the interface.
-proc ::p::internals::expand_interface {IID {force 0}} {
- #puts stdout ">>> expand_interface $IID [info level -1]<<<"
- if {![string length $IID]} {
- #return [::p::internals::new_interface] ;#new interface is by default open for extending (o_open = 1)
- set iid [expr {$::p::ID + 1}]
- ::p::>interface .. Create ::p::ifaces::>$iid
- return $iid
- } else {
- if {[set ::p::${IID}::_iface::o_open]} {
- #interface open for extending - shared or not!
- return $IID
- }
-
- if {[array size ::p::${IID}::_iface::o_usedby] > 1} {
- #upvar #0 ::p::${IID}::_iface::o_usedby prev_usedby
-
- #oops.. shared interface. Copy before specialising it.
- set prev_IID $IID
-
- #set IID [::p::internals::new_interface]
- set IID [expr {$::p::ID + 1}]
- ::p::>interface .. Create ::p::ifaces::>$IID
-
- ::p::internals::linkcopy_interface $prev_IID $IID
- #assert: prev_usedby contains at least one other element.
- }
-
- #whether copied or not - mark as open for extending.
- set ::p::${IID}::_iface::o_open 1
- return $IID
- }
-}
-
-#params: old - old (shared) interface ID
-# new - new interface ID
-proc ::p::internals::linkcopy_interface {old new} {
- #puts stderr " ** ** ** linkcopy_interface $old $new"
- set ns_old ::p::${old}::_iface
- set ns_new ::p::${new}::_iface
-
-
-
- foreach nsmethod [info commands ${ns_old}::*.1] {
- #puts ">>> adding $nsmethod to iface $new"
- set tail [namespace tail $nsmethod]
- set method [string range $tail 0 end-2] ;#strip .1
-
- if {![llength [info commands ${ns_new}::$method]]} {
-
- set oldhead [interp alias {} ${ns_old}::$method] ;#the 'head' of the cmdchain that it actually points to ie $method.$x where $x >=1
-
- #link from new interface namespace to existing one.
- #(we assume that since ${ns_new}::$method didn't exist, that all the $method.$x chain slots are empty too...)
- #!todo? verify?
- #- actual link is chainslot to chainslot
- interp alias {} ${ns_new}::$method.1 {} $oldhead
-
- #!todo - review. Shouldn't we be linking entire chain, not just creating a single .1 pointer to the old head?
-
-
- #chainhead pointer within new interface
- interp alias {} ${ns_new}::$method {} ${ns_new}::$method.1
-
- namespace eval $ns_new "namespace export $method"
-
- #if {[string range $method 0 4] ni {(GET) (SET) (UNSE (CONS }} {
- # lappend ${ns_new}::o_methods $method
- #}
- } else {
- if {$method eq "(VIOLATE)"} {
- #ignore for now
- #!todo
- continue
- }
-
- #!todo - handle how?
- #error "command $cmd already exists in interface $new"
-
- #warning - existing chainslot will be completely shadowed by linked method.
- # - existing one becomes unreachable. #!todo review!?
-
-
- error "linkcopy_interface $old -> $new - chainslot shadowing not implemented (method $method already exists on target interface $new)"
-
- }
- }
-
-
- #foreach propinf [set ${ns_old}::o_properties] {
- # lassign $propinf prop _default
- # #interp alias {} ${ns_new}::(GET)$prop {} ::p::predator::getprop $prop
- # #interp alias {} ${ns_new}::(SET)$prop {} ::p::predator::setprop $prop
- # lappend ${ns_new}::o_properties $propinf
- #}
-
-
- set ${ns_new}::o_variables [set ${ns_old}::o_variables]
- set ${ns_new}::o_properties [set ${ns_old}::o_properties]
- set ${ns_new}::o_methods [set ${ns_old}::o_methods]
- set ${ns_new}::o_constructor [set ${ns_old}::o_constructor]
-
-
- set ::p::${old}::_iface::o_usedby(i$new) linkcopy
-
-
- #obsolete.?
- array set ::p::${new}:: [array get ::p::${old}:: ]
-
-
-
- #!todo - is this done also when iface compiled?
- #namespace eval ::p::${new}::_iface {namespace ensemble create}
-
-
- #puts stderr "copy_interface $old $new"
-
- #assume that the (usedby) data is now obsolete
- #???why?
- #set ${ns_new}::(usedby) [::list]
-
- #leave ::(usedby) reference in place for caller to change as appropriate - 'copy'
-
- return
-}
-################################################################################################################################################
-################################################################################################################################################
-################################################################################################################################################
-
-pattern::init
-
-return $::pattern::version
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patterncmd-1.2.4.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patterncmd-1.2.4.tm
deleted file mode 100644
index ca061a7c..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patterncmd-1.2.4.tm
+++ /dev/null
@@ -1,645 +0,0 @@
-package provide patterncmd [namespace eval patterncmd {
- variable version
-
- set version 1.2.4
-}]
-
-
-namespace eval pattern {
- variable idCounter 1 ;#used by pattern::uniqueKey
-
- namespace eval cmd {
- namespace eval util {
- package require overtype
- variable colwidths_lib [dict create]
- variable colwidths_lib_default 15
-
- dict set colwidths_lib "library" [list ch " " num 21 head "|" tail ""]
- dict set colwidths_lib "version" [list ch " " num 7 head "|" tail ""]
- dict set colwidths_lib "type" [list ch " " num 9 head "|" tail ""]
- dict set colwidths_lib "note" [list ch " " num 31 head "|" tail "|"]
-
- proc colhead {type args} {
- upvar #0 ::pattern::cmd::util::colwidths_$type colwidths
- set line ""
- foreach colname [dict keys $colwidths] {
- append line "[col $type $colname [string totitle $colname] {*}$args]"
- }
- return $line
- }
- proc colbreak {type} {
- upvar #0 ::pattern::cmd::util::colwidths_$type colwidths
- set line ""
- foreach colname [dict keys $colwidths] {
- append line "[col $type $colname {} -backchar - -headoverridechar + -tailoverridechar +]"
- }
- return $line
- }
- proc col {type col val args} {
- # args -head bool -tail bool ?
- #----------------------------------------------------------------------------
- set known_opts [list -backchar -headchar -tailchar -headoverridechar -tailoverridechar -justify]
- dict set default -backchar ""
- dict set default -headchar ""
- dict set default -tailchar ""
- dict set default -headoverridechar ""
- dict set default -tailoverridechar ""
- dict set default -justify "left"
- if {([llength $args] % 2) != 0} {
- error "(pattern::cmd::util::col) ERROR: uneven options supplied - must be of form '-option value' "
- }
- foreach {k v} $args {
- if {$k ni $known_opts} {
- error "((pattern::cmd::util::col) ERROR: option '$k' not in known options: '$known_opts'"
- }
- }
- set opts [dict merge $default $args]
- set backchar [dict get $opts -backchar]
- set headchar [dict get $opts -headchar]
- set tailchar [dict get $opts -tailchar]
- set headoverridechar [dict get $opts -headoverridechar]
- set tailoverridechar [dict get $opts -tailoverridechar]
- set justify [dict get $opts -justify]
- #----------------------------------------------------------------------------
-
-
-
- upvar #0 ::pattern::cmd::util::colwidths_$type colwidths
- #calculate headwidths
- set headwidth 0
- set tailwidth 0
- foreach {key def} $colwidths {
- set thisheadlen [string length [dict get $def head]]
- if {$thisheadlen > $headwidth} {
- set headwidth $thisheadlen
- }
- set thistaillen [string length [dict get $def tail]]
- if {$thistaillen > $tailwidth} {
- set tailwidth $thistaillen
- }
- }
-
-
- set spec [dict get $colwidths $col]
- if {[string length $backchar]} {
- set ch $backchar
- } else {
- set ch [dict get $spec ch]
- }
- set num [dict get $spec num]
- set headchar [dict get $spec head]
- set tailchar [dict get $spec tail]
-
- if {[string length $headchar]} {
- set headchar $headchar
- }
- if {[string length $tailchar]} {
- set tailchar $tailchar
- }
- #overrides only apply if the head/tail has a length
- if {[string length $headchar]} {
- if {[string length $headoverridechar]} {
- set headchar $headoverridechar
- }
- }
- if {[string length $tailchar]} {
- if {[string length $tailoverridechar]} {
- set tailchar $tailoverridechar
- }
- }
- set head [string repeat $headchar $headwidth]
- set tail [string repeat $tailchar $tailwidth]
-
- set base [string repeat $ch [expr {$headwidth + $num + $tailwidth}]]
- if {$justify eq "left"} {
- set left_done [overtype::left $base "$head$val"]
- return [overtype::right $left_done "$tail"]
- } elseif {$justify in {centre center}} {
- set mid_done [overtype::centre $base $val]
- set left_mid_done [overtype::left $mid_done $head]
- return [overtype::right $left_mid_done $tail]
- } else {
- set right_done [overtype::right $base "$val$tail"]
- return [overtype::left $right_done $head]
- }
-
- }
-
- }
- }
-
-}
-
-#package require pattern
-
-proc ::pattern::libs {} {
- set libs [list \
- pattern {-type core -note "alternative:pattern2"}\
- pattern2 {-type core -note "alternative:pattern"}\
- patterncmd {-type core}\
- metaface {-type core}\
- patternpredator2 {-type core}\
- patterndispatcher {-type core}\
- patternlib {-type core}\
- patterncipher {-type optional -note optional}\
- ]
-
-
-
- package require overtype
- set result ""
-
- append result "[cmd::util::colbreak lib]\n"
- append result "[cmd::util::colhead lib -justify centre]\n"
- append result "[cmd::util::colbreak lib]\n"
- foreach libname [dict keys $libs] {
- set libinfo [dict get $libs $libname]
-
- append result [cmd::util::col lib library $libname]
- if {[catch [list package present $libname] ver]} {
- append result [cmd::util::col lib version "N/A"]
- } else {
- append result [cmd::util::col lib version $ver]
- }
- append result [cmd::util::col lib type [dict get $libinfo -type]]
-
- if {[dict exists $libinfo -note]} {
- set note [dict get $libinfo -note]
- } else {
- set note ""
- }
- append result [cmd::util::col lib note $note]
- append result "\n"
- }
- append result "[cmd::util::colbreak lib]\n"
- return $result
-}
-
-proc ::pattern::record {recname fields} {
- if {[uplevel 1 [list namespace which $recname]] ne ""} {
- error "(pattern::record) Can't create command '$recname': A command of that name already exists"
- }
-
- set index -1
- set accessor [list ::apply {
- {index rec args}
- {
- if {[llength $args] == 0} {
- return [lindex $rec $index]
- }
- if {[llength $args] == 1} {
- return [lreplace $rec $index $index [lindex $args 0]]
- }
- error "Invalid number of arguments."
- }
-
- }]
-
- set map {}
- foreach field $fields {
- dict set map $field [linsert $accessor end [incr index]]
- }
- uplevel 1 [list namespace ensemble create -command $recname -map $map -parameters rec]
-}
-proc ::pattern::record2 {recname fields} {
- if {[uplevel 1 [list namespace which $recname]] ne ""} {
- error "(pattern::record) Can't create command '$recname': A command of that name already exists"
- }
-
- set index -1
- set accessor [list ::apply]
-
- set template {
- {rec args}
- {
- if {[llength $args] == 0} {
- return [lindex $rec %idx%]
- }
- if {[llength $args] == 1} {
- return [lreplace $rec %idx% %idx% [lindex $args 0]]
- }
- error "Invalid number of arguments."
- }
- }
-
- set map {}
- foreach field $fields {
- set body [string map [list %idx% [incr index]] $template]
- dict set map $field [list ::apply $body]
- }
- uplevel 1 [list namespace ensemble create -command $recname -map $map -parameters rec]
-}
-
-proc ::argstest {args} {
- package require cmdline
-
-}
-
-proc ::pattern::objects {} {
- set result [::list]
-
- foreach ns [namespace children ::pp] {
- #lappend result [::list [namespace tail $ns] [set ${ns}::(self)]]
- set ch [namespace tail $ns]
- if {[string range $ch 0 2] eq "Obj"} {
- set OID [string range $ch 3 end] ;#OID need not be digits (!?)
- lappend result [::list $OID [list OID $OID object_command [set pp::${ch}::v_object_command] usedby [array names ${ns}::_iface::o_usedby]]]
- }
- }
-
-
-
-
- return $result
-}
-
-
-
-proc ::pattern::name {num} {
- #!todo - fix
- #set ::p::${num}::(self)
-
- lassign [interp alias {} ::p::$num] _predator info
- if {![string length $_predator$info]} {
- error "No object found for num:$num (no interp alias for ::p::$num)"
- }
- set invocants [dict get $info i]
- set invocants_with_role_this [dict get $invocants this]
- set invocant_this [lindex $invocants_with_role_this 0]
-
-
- #lassign $invocant_this id info
- #set map [dict get $info map]
- #set fields [lindex $map 0]
- lassign $invocant_this _id _ns _defaultmethod name _etc
- return $name
-}
-
-
-proc ::pattern::with {cmd script} {
- foreach c [info commands ::p::-1::*] {
- interp alias {} [namespace tail $c] {} $c $cmd
- }
- interp alias {} . {} $cmd .
- interp alias {} .. {} $cmd ..
-
- return [uplevel 1 $script]
-}
-
-
-
-
-
-#system diagnostics etc
-
-proc ::pattern::varspace_list {IID} {
- namespace upvar ::p::${IID}::_iface o_varspace o_varspace o_variables o_variables
-
- set varspaces [list]
- dict for {vname vdef} $o_variables {
- set vs [dict get $vdef varspace]
- if {$vs ni $varspaces} {
- lappend varspaces $vs
- }
- }
- if {$o_varspace ni $varspaces} {
- lappend varspaces $o_varspace
- }
- return $varspaces
-}
-
-proc ::pattern::check_interfaces {} {
- foreach ns [namespace children ::p] {
- set IID [namespace tail $ns]
- if {[string is digit $IID]} {
- foreach ref [array names ${ns}::_iface::o_usedby] {
- set OID [string range $ref 1 end]
- if {![namespace exists ::p::${OID}::_iface]} {
- puts -nonewline stdout "\r\nPROBLEM!!!!!!!!! nonexistant/invalid object $OID referenced by Interface $IID\r\n"
- } else {
- puts -nonewline stdout .
- }
-
-
- #if {![info exists ::p::${OID}::(self)]} {
- # puts "PROBLEM!!!!!!!!! nonexistant object $OID referenced by Interface $IID"
- #}
- }
- }
- }
- puts -nonewline stdout "\r\n"
-}
-
-
-#from: http://wiki.tcl.tk/8766 (Introspection on aliases)
-#usedby: metaface-1.1.6+
-#required because aliases can be renamed.
-#A renamed alias will still return it's target with 'interp alias {} oldname'
-# - so given newname - we require which_alias to return the same info.
- proc ::pattern::which_alias {cmd} {
- uplevel 1 [list ::trace add execution $cmd enterstep ::error]
- catch {uplevel 1 $cmd} res
- uplevel 1 [list ::trace remove execution $cmd enterstep ::error]
- #puts stdout "which_alias $cmd returning '$res'"
- return $res
- }
-# [info args] like proc following an alias recursivly until it reaches
-# the proc it originates from or cannot determine it.
-# accounts for default parameters set by interp alias
-#
-
-
-
-proc ::pattern::aliasargs {cmd} {
- set orig $cmd
-
- set defaultargs [list]
-
- # loop until error or return occurs
- while {1} {
- # is it a proc already?
- if {[string equal [info procs $cmd] $cmd]} {
- set result [info args $cmd]
- # strip off the interp set default args
- return [lrange $result [llength $defaultargs] end]
- }
- # is it a built in or extension command we can get no args for?
- if {![string equal [info commands $cmd] $cmd]} {
- error "\"$orig\" isn't a procedure"
- }
-
- # catch bogus cmd names
- if {[lsearch [interp aliases {}] $cmd]==-1} {
- if {[catch {::pattern::which_alias $cmd} alias]} {
- error "\"$orig\" isn't a procedure or alias or command"
- }
- #set cmd [lindex $alias 0]
- if {[llength $alias]>1} {
- set cmd [lindex $alias 0]
- set defaultargs [concat [lrange $alias 1 end] $defaultargs]
- } else {
- set cmd $alias
- }
- } else {
-
- if {[llength [set cmdargs [interp alias {} $cmd]]]>0} {
- # check if it is aliased in from another interpreter
- if {[catch {interp target {} $cmd} msg]} {
- error "Cannot resolve \"$orig\", alias leads to another interpreter."
- }
- if {$msg != {} } {
- error "Not recursing into slave interpreter \"$msg\".\
- \"$orig\" could not be resolved."
- }
- # check if defaults are set for the alias
- if {[llength $cmdargs]>1} {
- set cmd [lindex $cmdargs 0]
- set defaultargs [concat [lrange $cmdargs 1 end] $defaultargs]
- } else {
- set cmd $cmdargs
- }
- }
- }
- }
- }
-proc ::pattern::aliasbody {cmd} {
- set orig $cmd
-
- set defaultargs [list]
-
- # loop until error or return occurs
- while {1} {
- # is it a proc already?
- if {[string equal [info procs $cmd] $cmd]} {
- set result [info body $cmd]
- # strip off the interp set default args
- return $result
- #return [lrange $result [llength $defaultargs] end]
- }
- # is it a built in or extension command we can get no args for?
- if {![string equal [info commands $cmd] $cmd]} {
- error "\"$orig\" isn't a procedure"
- }
-
- # catch bogus cmd names
- if {[lsearch [interp aliases {}] $cmd]==-1} {
- if {[catch {::pattern::which_alias $cmd} alias]} {
- error "\"$orig\" isn't a procedure or alias or command"
- }
- #set cmd [lindex $alias 0]
- if {[llength $alias]>1} {
- set cmd [lindex $alias 0]
- set defaultargs [concat [lrange $alias 1 end] $defaultargs]
- } else {
- set cmd $alias
- }
- } else {
-
- if {[llength [set cmdargs [interp alias {} $cmd]]]>0} {
- # check if it is aliased in from another interpreter
- if {[catch {interp target {} $cmd} msg]} {
- error "Cannot resolve \"$orig\", alias leads to another interpreter."
- }
- if {$msg != {} } {
- error "Not recursing into slave interpreter \"$msg\".\
- \"$orig\" could not be resolved."
- }
- # check if defaults are set for the alias
- if {[llength $cmdargs]>1} {
- set cmd [lindex $cmdargs 0]
- set defaultargs [concat [lrange $cmdargs 1 end] $defaultargs]
- } else {
- set cmd $cmdargs
- }
- }
- }
- }
- }
-
-
-
-
-
-proc ::pattern::uniqueKey2 {} {
- #!todo - something else??
- return [clock seconds]-[incr ::pattern::idCounter]
-}
-
-#used by patternlib package
-proc ::pattern::uniqueKey {} {
- return [incr ::pattern::idCounter]
- #uuid with tcllibc is about 30us compared with 2us
- # for large datasets, e.g about 100K inserts this would be pretty noticable!
- #!todo - uuid pool with background thread to repopulate when idle?
- #return [uuid::uuid generate]
-}
-
-
-
-#-------------------------------------------------------------------------------------------------------------------------
-
-proc ::pattern::test1 {} {
- set msg "OK"
-
- puts stderr "next line should say:'--- saystuff:$msg"
- ::>pattern .. Create ::>thing
-
- ::>thing .. PatternMethod saystuff args {
- puts stderr "--- saystuff: $args"
- }
- ::>thing .. Create ::>jjj
-
- ::>jjj . saystuff $msg
- ::>jjj .. Destroy
- ::>thing .. Destroy
-}
-
-proc ::pattern::test2 {} {
- set msg "OK"
-
- puts stderr "next line should say:'--- property 'stuff' value:$msg"
- ::>pattern .. Create ::>thing
-
- ::>thing .. PatternProperty stuff $msg
-
- ::>thing .. Create ::>jjj
-
- puts stderr "--- property 'stuff' value:[::>jjj . stuff]"
- ::>jjj .. Destroy
- ::>thing .. Destroy
-}
-
-proc ::pattern::test3 {} {
- set msg "OK"
-
- puts stderr "next line should say:'--- property 'stuff' value:$msg"
- ::>pattern .. Create ::>thing
-
- ::>thing .. Property stuff $msg
-
- puts stderr "--- property 'stuff' value:[::>thing . stuff]"
- ::>thing .. Destroy
-}
-
-#---------------------------------
-#unknown/obsolete
-
-
-
-
-
-
-
-
-#proc ::p::internals::showargs {args {ch stdout}} {puts $ch $args}
-if {0} {
- proc ::p::internals::new_interface {{usedbylist {}}} {
- set OID [incr ::p::ID]
- ::p::internals::new_object ::p::ifaces::>$OID "" $OID
- puts "obsolete >> new_interface created object $OID"
- foreach usedby $usedbylist {
- set ::p::${OID}::_iface::o_usedby(i$usedby) 1
- }
- set ::p::${OID}::_iface::o_varspace "" ;#default varspace is the object's namespace. (varspace is absolute if it has leading :: , otherwise it's a relative namespace below the object's namespace)
- #NOTE - o_varspace is only the default varspace for when new methods/properties are added.
- # it is possible to create some methods/props with one varspace value, then create more methods/props with a different varspace value.
-
- set ::p::${OID}::_iface::o_constructor [list]
- set ::p::${OID}::_iface::o_variables [list]
- set ::p::${OID}::_iface::o_properties [dict create]
- set ::p::${OID}::_iface::o_methods [dict create]
- array set ::p::${OID}::_iface::o_definition [list]
- set ::p::${OID}::_iface::o_open 1 ;#open for extending
- return $OID
- }
-
-
- #temporary way to get OID - assumes single 'this' invocant
- #!todo - make generic.
- proc ::pattern::get_oid {_ID_} {
- #puts stderr "#* get_oid: [lindex [dict get $_ID_ i this] 0 0]"
- return [lindex [dict get $_ID_ i this] 0 0]
-
- #set invocants [dict get $_ID_ i]
- #set invocant_roles [dict keys $invocants]
- #set role_members [dict get $invocants this]
- ##set this_invocant [lindex $role_members 0] ;#for the role 'this' we assume only one invocant in the list.
- #set this_invocant [lindex [dict get $_ID_ i this] 0] ;
- #lassign $this_invocant OID this_info
- #
- #return $OID
- }
-
- #compile the uncompiled level1 interface
- #assert: no more than one uncompiled interface present at level1
- proc ::p::meta::PatternCompile {self} {
- ????
-
- upvar #0 $self SELFMAP
- set ID [lindex $SELFMAP 0 0]
-
- set patterns [lindex $SELFMAP 1 1] ;#list of level1 interfaces
-
- set iid -1
- foreach i $patterns {
- if {[set ::p::${i}::_iface::o_open]} {
- set iid $i ;#found it
- break
- }
- }
-
- if {$iid > -1} {
- #!todo
-
- ::p::compile_interface $iid
- set ::p::${iid}::_iface::o_open 0
- } else {
- #no uncompiled interface present at level 1. Do nothing.
- return
- }
- }
-
-
- proc ::p::meta::Def {self} {
- error ::p::meta::Def
-
- upvar #0 $self SELFMAP
- set self_ID [lindex $SELFMAP 0 0]
- set IFID [lindex $SELFMAP 1 0 end]
-
- set maxc1 0
- set maxc2 0
-
- set arrName ::p::${IFID}::
-
- upvar #0 $arrName state
-
- array set methods {}
-
- foreach nm [array names state] {
- if {[regexp {^m-1,name,(.+)} $nm _match mname]} {
- set methods($mname) [set state($nm)]
-
- if {[string length $mname] > $maxc1} {
- set maxc1 [string length $mname]
- }
- if {[string length [set state($nm)]] > $maxc2} {
- set maxc2 [string length [set state($nm)]]
- }
- }
- }
- set bg1 [string repeat " " [expr {$maxc1 + 2}]]
- set bg2 [string repeat " " [expr {$maxc2 + 2}]]
-
-
- set r {}
- foreach nm [lsort -dictionary [array names methods]] {
- set arglist $state(m-1,args,$nm)
- append r "[overtype::left $bg1 $nm] : [overtype::left $bg2 $methods($nm)] [::list $arglist]\n"
- }
- return $r
- }
-
-
-
-}
\ No newline at end of file
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patternlib-1.2.6.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patternlib-1.2.6.tm
deleted file mode 100644
index bd4b3e59..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patternlib-1.2.6.tm
+++ /dev/null
@@ -1,2590 +0,0 @@
-#JMN 2004
-#public domain
-
-
-package provide patternlib [namespace eval patternlib {
-
- variable version
- set version 1.2.6
-}]
-
-
-
-#Change History
-#-------------------------------------------------------------------------------
-#2022-05
-# added . search and . itemKeys methods to >collection to enable lookups by value
-#2021-09
-# Add >keyvalprotector - an object to overload various collection methods such as 'remove' to stop deletion of specific items.
-#
-#2006-05
-# deprecate 'del' in favour of 'remove' - 'del' still there but delegated to 'remove'. todo - emit deprecation warnings.
-#
-#2005-04
-# remove 'name' method - incorporate indexed retrieval into 'names' method
-# !todo? - adjust key/keys methods for consistency?
-#
-#2004-10
-# initial key aliases support
-# fix negative index support on some methods e.g remove
-#2004-08
-# separated >collection predicate methods out onto separate 'mixin' object >predicatedCollection
-# added $posn $result variables to predicate methods, changed varnames from $k $v to $key $value
-#
-#2004-06-05
-# added 'sort' method to sort on values.
-# fixed 'keySort' method to accept multiple sort options
-# added predicate methods 'all' 'allKeys' 'collectAll'
-#2004-06-01
-# '>collection . names' method now accepts optional 'glob' parameter to filter result
-#2004-05-19
-#fix '>collection . clear' method so consecutive calls don't raise an error
-#-------------------------------------------------------------------------------
-
-namespace eval ::patternlib::util {
- proc package_require_min {pkg minver} {
- if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} {
- package require $pkg
- } else {
- error "Package pattern requires package $pkg of at least version $minver. Available: $available"
- }
- }
-
- #bloom filter experiment https://wiki.tcl-lang.org/page/A+Simple+Bloom+Filter
- # k-hashes
- # m-bits
- # n-elements
- # optimal value of k: (m/n)ln(2)
- #proc bloom_optimalNumHashes {capacity_n bitsize_m} {
- # expr { round((double($bitsize_m) / $capacity_n) * log(2))}
- #}
- #proc bloom_optimalNumBits {capacity fpp} {
- # expr {entier(-$capacity * log($fpp) / (log(2) * log(2)))}
- #}
-
-}
-::patternlib::util::package_require_min pattern 1.2.4
-#package require pattern
-::pattern::init ;# initialises (if not already)
-
-
-namespace eval ::patternlib {namespace export {[a-z]*}
- namespace export {[>]*}
-
- variable keyCounter 0 ;#form part of unique keys for collections when items added without any key specified
- proc uniqueKey {} {
- return [incr ::patternlib::keyCounter]
- }
-
-#!todo - multidimensional collection
-# - o_list as nested list
-# - o_array with compound keys(?) how will we unambiguously delimit dimensions in a concatenated key?
-# - perhaps a key is always a list length n where n is the number of dimensions?
-# - therefore we'll need an extra level of nesting for the current base case n=1
-#
-# - how about a nested dict for each key-structure (o_list & o_array) ?
-
-#COLLECTION
-#
-#!todo? - consider putting the actual array & list vars in the objects namespace, and using the instancevars to hold their names
-# - consider array-style access using traced var named same as collection.
-# would this defeat the purpose ? if it was faster, would users always use array syntax in preference.. in which case they may as well just use arrays..?
-#!todo - add boolean property to force unique values as well as keys
-
-
-#::pattern::create >collection
-
-
-
-
-::>pattern .. Create >collection
-set COL >collection
-#process_pattern_aliases [namespace origin >collection]
-#process_pattern_aliases ::patternlib::>collection
-$COL .. Property version 1.0
-$COL .. PatternDefaultMethod item
-
-set PV [$COL .. PatternVariable .]
-
-$PV o_data
-#$PV o_array
-#$PV o_list
-$PV o_alias
-$PV this
-
-#for invert method
-$PV o_dupes 0
-
-
-$COL .. PatternProperty bgEnum
-
-
-#PV o_ns
-
-$PV m_i_filteredCollection
-
-#set ID [lindex [set >collection] 0 0] ;#context ID
-#set IID [lindex [set >collection] 1 0] ;#level 1 base-interface ID
-
-$COL .. Constructor {args} {
- var o_data m_i_filteredCollection o_count o_bgEnum
-
- var this
- set this @this@
-
- set m_i_filteredCollection 0
- if {![llength $args]} {
- set o_data [dict create]
- #array set o_array [list]
- #set o_list [list]
- set o_count 0
- } elseif {[llength $args] == 1} {
- set o_data [dict create]
- set pairs [lindex $args 0]
- if {[llength $pairs] % 2} {
- error "patternllib::>collection - if an argument given to constructor, it must have an even number of elements. Bad args: $args"
- }
- set keys_seen [list]
- foreach key [dict keys $pairs] {
- if {[string is integer -strict $key] } {
- error ">collection key must be non-integer. Bad key: $key. No items added."
- }
- if {$key in $keys_seen} {
- error "key '$key' already exists in this collection. No items added."
- }
- lappend keys_seen $key
- }
- unset keys_seen
- #rely on dict ordering guarantees (post 8.5? preserves order?)
- set o_data [dict merge $o_data[set o_data {}] $pairs]
- set o_count [dict size $o_data]
- } else {
- error "patternlib::>collection constructor did not understand arguments supplied. Try a dict as a single argument."
- }
- array set o_alias [list]
-
- array set o_bgEnum [list]
- @next@
-}
-#comment block snipped from collection Constructor
- #---------------------------------------------
- #set o_selfID [lindex [set $o_this] 0] ;#object id always available in methods as $_ID_ anyway
- #
- #### OBSOLETE - left as example of an approach
- #make count property traceable (e.g so property ref can be bound to Tk widgets)
- #!todo - manually update o_count in relevant methods faster??
- # should avoid trace calls for addList methods, shuffle etc
- #
- #set handler ::p::${_ID_}::___count_TraceHandler
- #proc $handler {_ID_ vname vidx op} {
- # #foreach {vname vidx op} [lrange $args end-2 end] {break}
- # #! we shouldn't trust this vname - it may be that we are being accessed via upvar so it is a different name
- #
- # #this is only a 'write' handler
- # set ::p::[lindex ${_ID_} 0 0]::o_count [llength [set ::p::[lindex ${_ID_} 0 0]::o_list]]
- # return
- #}
- #trace add variable o_list {write} [list $handler $_ID_]
- ####
- #
- #
- #puts "--->collection constructor id: $_ID_"
-
-
-
-
-set PM [$COL .. PatternMethod .]
-
-
-#!review - why do we need the count method as well as the property?
-#if needed - document why.
-# read traces on count property can be bypassed by method call... shouldn't we avoid that?
-#2018 - in theory write traces on the . count property are very useful from an application-writer's perpective.
-#
-$COL .. PatternMethod count {} {
- #we don't require any instance vars to be upvar'ed - argless [var] stops them automatically being added.
- #we directly refer to the ::O:: var if only accessing a few times rather than upvar'ing.
- var o_data
- dict size $o_data
-}
-
-$COL .. PatternProperty count
-$COL .. PatternPropertyWrite count {_val} {
- var
- error "count property is read-only"
-}
-
-$COL .. PatternPropertyUnset count {} {
- var
-} ;#cannot raise error's in unset trace handlers - simply fail to unset silently
-
-$COL .. PatternMethod isEmpty {} {
- #var o_list
- #return [expr {[llength $o_list] == 0}]
- var o_data
- expr {[dict size $o_data] == 0}
-}
-
-$COL .. PatternProperty inverted 0
-
-
-
-######
-# item
-######
-#defaults to fifo when no idx supplied (same as 'pair' method). !review? is lifo more logical/intuitive/useful?
-# i.e [>obj . item] returns the 1st element in the list
-#[>obj . item -1] returns the last element (equiv to "end" keyword used by Tcl list commands)
-#[>obj . item -2] returns 2nd last element (equiv to "end-1")
-
-
-$COL .. PatternMethod item {{idx 0}} {
- #with pattern::0::$OID access.. was measured faster than item2 : approx 110us vs 140us for 26element collection accessed via string (time {>col $key} 10000)
- # (still at least 20 times slower than a plain array... at <5us)
- var o_data o_alias
-
- #!todo - review 'string is digit' vs 'string is integer' ??
- if {[string is integer -strict $idx]} {
- if {$idx < 0} {
- set idx "end-[expr {abs($idx + 1)}]"
- }
- set keys [dict keys $o_data]
- if {[catch {dict get $o_data [lindex $keys $idx]} result]} {
- var this
- error "no such index : '$idx' in collection: $this"
- } else {
- return $result
- }
- } else {
- if {[catch {dict get $o_data $idx} result]} {
- if {[catch {set o_alias($idx)} nextIdx ]} {
- var this
- error "no such index: '$idx' in collection: $this"
- } else {
- #try again
- #return $o_array($nextIdx)
- #tailcall?
- #item $_ID_ $nextIdx
- #puts stdout "\n\n\n !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! about to call tailcall item $_ID_ $nextIdx \n\n\n"
- tailcall item $_ID_ $nextIdx
- }
- } else {
- return $result
- }
- }
-}
-
-
-
-if {0} {
-#leave this here for comparison.
-$COL .. PatternMethod item2 {{idx 0}} {
- var o_array o_list o_alias this
-
- if {[string is integer -strict $idx]} {
- if {$idx < 0} {
- set idx "end-[expr {abs($idx + 1)}]"
- }
-
- if {[catch {set o_array([lindex $o_list $idx])} result]} {
- error "no such index : '$idx' in collection: $this"
- } else {
- return $result
- }
- } else {
- if {[catch {set o_array($idx)} result]} {
-
- if {[catch {set o_alias($idx)} nextIdx ]} {
- error "no such index: '$idx' in collection: $this"
- } else {
- #try again
- #return $o_array($nextIdx)
- item $_ID_ $nextIdx
- }
- } else {
- return $result
- }
- }
-
-}
-}
-
-#simple no-frills access for speed.. (timed at 43us vs 63us for item (depending on dispatch method!))
-$COL .. PatternMethod itemNamed {idx} {
- var o_data
- dict get $o_data $idx
-}
-$COL .. PatternMethod in {idx} {
- var o_data
- dict get $o_data $idx
-}
-
-$COL .. PatternMethod itemAt {idx} {
- var o_data
- dict get $o_data [lindex [dict keys $o_data] $idx]
-}
-
-$COL .. PatternMethod replace {idx val} {
- var o_data o_alias this
-
- if {[string is integer -strict $idx]} {
- if {$idx < 0} {
- set idx "end-[expr {abs($idx + 1)}]"
- }
-
- if {[catch {dict set o_data [lindex [dict keys $o_data] $idx] $val}]} {
- error "no such index: '$idx' in collection: $this"
- } else {
- return $val
- }
- } else {
- if {[catch {dict set o_data $idx $val}]} {
- if {[catch {set o_alias($idx)} nextIdx ]} {
- error "no such index: '$idx' in collection: $this"
- } else {
- #try again
- tailcall replace $_ID_ $nextIdx $val
- }
-
- } else {
- return $val
- }
- }
-}
-
-#if the supplied index is an alias, return the underlying key; else return the index supplied.
-$COL .. PatternMethod realKey {idx} {
- var o_alias
-
- if {[catch {set o_alias($idx)} key]} {
- return $idx
- } else {
- return $key
- }
-}
-
-#note alias feature is possibly ill-considered.
-#if we delete an item - should we delete corresponding alias? If not - we then would need to allow adding under an alias only if the corresponding key is missing.
-$COL .. PatternMethod alias {newAlias existingKeyOrAlias} {
- var o_alias
-
- #set existingKey [realKey $_ID_ $existingKeyOrAlias]
- #alias to the supplied KeyOrAlias - not the underlying key
-
- if {[string is integer -strict $newAlias]} {
- error "collection key alias cannot be integer"
- }
-
- if {[string length $existingKeyOrAlias]} {
- set o_alias($newAlias) $existingKeyOrAlias
- } else {
- unset o_alias($newAlias)
- }
-}
-$COL .. PatternMethod aliases {{key ""}} {
- var o_alias
-
- if {[string length $key]} {
- set result [list]
- #lsearch -stride?
- foreach {n v} [array get o_alias] {
- if {$v eq $key} {
- lappend result $n $v
- }
- }
-
- return $result
- } else {
- return [array get o_alias]
- }
-}
-
-#'pop' & 'unshift' methods !todo - optimize so lsearch not called when numerical idx/posn already supplied
-
-#default to removing item from the end, otherwise from supplied index (position or key)
-#!todo - accept alias indices
-#!todo - review.. should any corresponding alias be destroyed when the corresponding item is popped (or removed in any way?)
-#!todo - review.. for performance.. shouldn't pop NOT accept an index?
-#if we need to pop from other than the end.. this could be a separate function. Do other langs use pop with an index??
-$COL .. PatternMethod pop {{idx ""}} {
- var o_data o_count
-
- if {$idx eq ""} {
- set key [lindex [dict keys $o_data] end]
- } else {
- if {[string is integer -strict $idx]} {
- set key [lindex [dict keys $o_data] $idx]
- } else {
- set key $idx
- }
- }
- set posn [lsearch -exact [dict keys $o_data] $key]
-
- if {($posn >= 0) && ($posn < [dict size $o_data])} {
- set result [dict get $o_data $key]
- dict unset o_data $key
- set o_count [dict size $o_data]
- return $result
- } else {
- error "no such index: '$idx'"
- }
-}
-$COL .. PatternMethod poppair {} {
- var o_data o_count
- set key [lindex [dict keys $o_data] end]
- set val [dict get $o_data $key]
- dict unset o_data $key
- set o_count [dict size $o_data]
- return [list $key $val]
-}
-
-
-
-#!todo - add 'push' method... (basically specialized versions of 'add')
-#push - add at end (effectively an alias for add)
-#shift - add at start ???bad name? this is completely at odds with for example the common Perl shift function, which returns and removes the first element of an array.
-#add - add at end
-
-#ordered
-$COL .. PatternMethod items {} {
- var o_data
-
- dict values $o_data
-}
-
-
-
-
-####
-#pair
-####
-#fifo-style accesss when no idx supplied (likewise with 'add' method)
-$COL .. PatternMethod pair {{idx 0}} {
- var o_data
-
- if {[string is integer -strict $idx]} {
- set key [lindex [dict keys $o_data] $idx]
- } else {
- set key $idx
- }
-
- if {[catch {dict get $o_data $key} val]} {
- error "no such index: '$idx'"
- } else {
- return [list $key $val]
- }
-}
-$COL .. PatternMethod pairs {} {
- var o_data
- set o_data
-}
-
-$COL .. PatternMethod get {} {
- var o_data
- set o_data
-}
-#todo - fix >pattern so that methods don't collide with builtins
-#may require change to use oo - or copy 'my' mechanism to call own methods
-$COL .. PatternMethod Info {} {
- var o_data
- return [dict info $o_data]
-}
-#2006-05-21.. args to add really should be in key, value order?
-# - this the natural order in array-like lists
-# - however.. key should be optional.
-
-$COL .. PatternMethod add {val args} {
- #(using args instead of {key ""} enables use of empty string as a key )
-
- var o_data o_alias o_count this
-
- if {![llength $args]} {
- set key "_[::patternlib::uniqueKey]_"
- } else {
- #!todo - could we handle multiple val,key pairs without impacting performance of the common case?
- if {[llength $args] > 1} {
- error "add method expected 'val' and optional 'key' - got: $val $args"
-
- }
-
- set key [lindex $args 0]
- if {[string is integer -strict $key]} {
- error ">collection key must be non-numeric. Other structures such as >hashMap allow user specified integer keys"
- }
- }
-
- if {[dict exists $o_data $key]} {
- #error "key $key already exists in collection [set ::p::[lindex ${_ID_} 0 0]::this]"
- error "key '$key' already exists in collection $this"
- }
- if {[info exists o_alias($key)]} {
- if {[dict exists $o_data $o_alias($key)]} {
- #only disallow adding via the alias if there is an existing o_data element for the key pointed to by the alias
- error "key '$key' already exists as an alias for $o_alias($key) in collection $this"
- }
- }
-
- dict set o_data $key $val
-
-
- set posn $o_count
- incr o_count
-
- return $posn
-}
-
-
-#should the 'stack' methods such as shift,push,pop,peek actually be on a separate interface?
-#what then of methods like 'count' which apply equally well to collections and stacks?
-
-#Alias for 'add' - is there a way to alias this to add implementation with zero overhead??
-$COL .. PatternMethod push {val args} {
- #(using args instead of {key ""} enables use of empty string as a key )
-
- var o_data o_alias o_count this
-
- if {![llength $args]} {
- set key "_[::patternlib::uniqueKey]_"
- } else {
- #!todo - could we handle multiple val,key pairs without impacting performance of the common case?
- if {[llength $args] > 1} {
- error "add method expected 'val' and optional 'key' - got: $val $args"
-
- }
-
- set key [lindex $args 0]
- if {[string is integer -strict $key]} {
- error ">collection key must be non-numeric. Other structures such as >hashMap allow user specified integer keys"
- }
- }
-
- if {[dict exists $o_data $key]} {
- #error "key $key already exists in collection [set ::p::[lindex ${_ID_} 0 0]::this]"
- error "key '$key' already exists in collection $this"
- }
- if {[info exists o_alias($key)]} {
- if {[dict exists $o_data $o_alias($key)]} {
- #only disallow adding via the alias if there is an existing o_data element for the key pointed to by the alias
- error "key '$key' already exists as an alias for $o_alias($key) in collection $this"
- }
- }
-
- dict set o_data $key $val
-
-
- set posn $o_count
- incr o_count
-
- return $posn
-}
-
-
-#shift/unshift - roughly analogous to those found in Perl & PHP
-#unshift adds 1 or more values to the beginning of the collection.
-$COL .. PatternMethod unshift {values {keys ""}} {
- var o_data o_count
-
- if {![llength $keys]} {
- for {set i 0} {$i < [llength $values]} {incr i} {
- lappend keys "_[::patternlib::uniqueKey]_"
- }
- } else {
- #check keys before we insert any of them.
- foreach newkey $keys {
- if {[string is integer -strict $newkey]} {
- error "cannot accept key '$newkey', >collection keys must be non-numeric. Other structures such as >hashMap allow user specified integer keys"
- }
- }
- }
- if {[llength $values] != [llength $keys]} {
- error "unshift requires same number of keys as values. (or no keys for auto-generated keys) Received [llength $values] values, [llength $keys] keys"
- }
-
- #separate loop through keys because we want to fail the whole operation if any are invalid.
-
- set existing_keys [dict keys $o_data]
- foreach newkey $keys {
- if {$newkey in $exisint_keys} {
- #puts stderr "==============> key $key already exists in this collection"
- error "key '$newkey' already exists in this collection"
- }
- }
-
-
- #ok - looks like entire set can be inserted.
- set newpairs [list]
- foreach val $values key $keys {
- lappend newpairs $key $val
- }
- set o_data [concat $newpairs $o_data[set o_data {}]]
- set o_count [dict size $o_data]
-
- return [expr {$o_count - 1}]
-}
-
-#default to removing item from the beginning, otherwise from supplied index (position or key)
-#!todo - accept alias indices
-$COL .. PatternMethod shift {{idx ""}} {
- var o_data o_count
-
- if {$idx eq ""} {
- set key [lindex [dict keys $o_data] 0]
- } else {
- if {[string is integer -strict $idx]} {
- set key [lindex [dict keys $o_data] $idx]
- } else {
- set key $idx
- }
- }
- set posn [lsearch -exact [dict keys $o_data] $key]
-
- if {($posn >= 0) && (($posn/2) < [dict size $o_data])} {
- set result [dict get $o_data $key]
- dict unset o_data $key
- set o_count [dict size $o_data]
- return $result
- } else {
- error "no such index: '$idx'"
- }
-}
-
-
-$COL .. PatternMethod peek {} {
- var o_data
-
- #set o_array([lindex $o_list end])
-
- #dict get $o_data [lindex [dict keys $o_data] end]
- lindex $o_data end
-}
-
-$COL .. PatternMethod peekKey {} {
- var o_data
- #lindex $o_list end
- lindex $o_data end-1
-}
-
-
-$COL .. PatternMethod insert {val args} {
- var o_data o_count
-
- set idx 0
- set key ""
-
- if {[llength $args] <= 2} {
- #standard arg (ordered) style:
- #>obj . insert $value $position $key
-
- lassign $args idx key
- } else {
- #allow for literate programming style:
- #e.g
- # >obj . insert $value at $listPosition as $key
-
- if {[catch {array set iargs $args}]} {
- error "insert did not understand argument list.
-usage:
->obj . insert \$val \$position \$key
->obj . insert \$val at \$position as \$key"
- }
- if {[info exists iargs(at)]} {
- set idx $iargs(at)
- }
- if {[info exists iargs(as)]} {
- set key $iargs(as)
- }
- }
-
- if {![string length $key]} {
- set key "_[::patternlib::uniqueKey]_"
- }
-
- if {[string is integer -strict $key]} {
- error ">collection key must be non-numeric. Other structures such as >hashMap allow user specified integer keys"
- }
-
-
- if {[dict exists $o_data $key]} {
- #puts stderr "==============> key $key already exists in this collection"
- error "key '$key' already exists in this collection"
- }
-
- if {$idx eq "end"} {
- #lappend o_list $key
- #standard dict set will add it to the end anyway
- dict set o_data $key $val
-
- } else {
- #set o_list [linsert $o_list $idx $key]
-
- #treat dict as list
- set o_data [linsert $o_data[set o_data {}] [expr {$idx*2}] $key $val]
- }
-
-
- #set o_array($key) $val
-
-
- set o_count [dict size $o_data]
-
- return [expr {$o_count - 1}]
-}
-
-#!todo - deprecate and give it a better name! addDict addPairs ?
-$COL .. PatternMethod addArray {list} {
- var
- puts stderr "patternlib::>collection WARNING: addArray deprecated - call addPairs with same argument instead"
- tailcall addPairs $_ID_ $list
-}
-$COL .. PatternMethod addPairs {list} {
- var o_data o_alias o_count
- if {[llength $list] % 2} {
- error "must supply an even number of elements"
- }
-
- set aliaslist [array names o_alias]
- #set keylist [dict keys $o_data]
- foreach newkey [dict keys $list] {
- if {[string is integer -strict $newkey] } {
- error ">collection key must be non-integer. Bad key: $newkey. No items added."
- }
-
- #if {$newkey in $keylist} {}
- #for small to medium collections - testing for newkey in $keylist is probably faster,
- # but we optimise here for potentially large existing collections, where presumably a dict exists lookup will be more efficient.
- if {[dict exists $o_data $newkey]} {
- error "key '$newkey' already exists in this collection. No items added."
- }
- #The assumption is that there are in general relatively few aliases - so a list test is appropriate
- if {$newkey in $aliaslist} {
- if {[dict exists $o_data $o_alias($newkey)]} {
- error "key '$newkey' already exists as an alias for $o_alias($newkey) in collection. No items added "
- }
- }
- #! check if $list contains dups?
- #- slows method down - for little benefit?
- }
- #!todo - test? (but we need a loop to test for integer keys.. so what's the point?)
- #set intersection [struct::set intersect [dict keys $list] [dict keys $o_data]]
- #if {[llength $intersection]} {
- # error "keys '$intersection' already present in this collection. No items added."
- #}
-
-
- #rely on dict ordering guarantees (post 8.5? preserves order?)
- set o_data [dict merge $o_data[set o_data {}] $list]
-
- set o_count [dict size $o_data]
-
- return [expr {$o_count - 1}]
-}
-$COL .. PatternMethod addList {list} {
- var o_data o_count
-
- foreach val $list {
- dict set o_data "_[::patternlib::uniqueKey]_" $val
- #!todo - test. Presumably lappend faster because we don't need to check existing keys..
- #..but.. is there shimmering involved in treating o_data as a list?
- #lappend o_data _[::patternlib::uniqueKey]_ $val
-
- #tested 2008-06 tcl8.6a0 lappend was slower as the gain is lost (and more!) during subsequent [dict size $o_data]
- }
- set o_count [dict size $o_data]
-
- return [expr {$o_count - 1}]
-}
-
-#'del' is not a very good name... as we're not really 'deleting' anything.
-# 'remove' seems better, and appears to be more consistent with other languages' collection implementations.
-#!todo - handle 'endRange' parameter for removing ranges of items.
-$COL .. PatternMethod del {idx {endRange ""}} {
- var
- #!todo - emit a deprecation warning for 'del'
- tailcall remove $_ID_ $idx $endRange
-}
-
-$COL .. PatternMethod remove {idx {endRange ""}} {
- var o_data o_count o_alias this
-
- if {[string length $endRange]} {
- error "ranged removal not yet implemented.. remove one item at a time."
- }
-
-
- if {[string is integer -strict $idx]} {
- if {$idx < 0} {
- set idx "end-[expr {abs($idx + 1)}]"
- }
- set key [lindex [dict keys $o_data] $idx]
- set posn $idx
- } else {
- set key $idx
- set posn [lsearch -exact [dict keys $o_data] $key]
- if {$posn < 0} {
- if {[catch {set o_alias($key)} nextKey]} {
- error "no such index: '$idx' in collection: $this"
- } else {
- #try with next key in alias chain...
- #return [remove $_ID_ $nextKey]
- tailcall remove $_ID_ $nextKey
- }
- }
- }
-
- dict unset o_data $key
-
- set o_count [dict size $o_data]
- return
-}
-
-#ordered
-$COL .. PatternMethod names {{globOrIdx {}}} {
- var o_data
-
- if {[llength $globOrIdx]} {
- if {[string is integer -strict $globOrIdx]} {
- #Idx
- set idx $globOrIdx
-
- if {$idx < 0} {
- set idx "end-[expr {abs($idx + 1)}]"
- }
-
-
-
- if {[catch {lindex [dict keys $o_data] $idx} result]} {
- error "no such index : '$idx'"
- } else {
- return $result
- }
-
- } else {
- #glob
- return [lsearch -glob -all -inline [dict keys $o_data] $globOrIdx]
- }
- } else {
- return [dict keys $o_data]
- }
-}
-
-#ordered
-$COL .. PatternMethod keys {} {
- #like 'names' but without globbing
- var o_data
- dict keys $o_data
-}
-
-#Unfortunately the string 'name' is highly collidable when mixing in a collection over existing objects
-# - !todo - review. Is it worth adjusting the collection methodnames to avoid a few common collision cases?
-# - some sort of resolution order/interface-selection is clearly required anyway
-# so perhaps it's generally best not to bother being 'polite' here, and implement a robust understandable resolution mechanism.
-# In the mean time however... we'll at least avoid 'name'!
-#
-#$PM name {{posn 0}} {
-# var o_array o_list
-#
-# if {$posn < 0} {
-# set posn "end-[expr {abs($posn + 1)}]"
-# }
-#
-# if {[catch {lindex $o_list $posn} result]} {
-# error "no such index : '$posn'"
-# } else {
-# return $result
-# }
-#}
-
-$COL .. PatternMethod key {{posn 0}} {
- var o_data
-
- if {$posn < 0} {
- set posn "end-[expr {abs($posn + 1)}]"
- }
-
- if {[catch {lindex [dict keys $o_data] $posn} result]} {
- error "no such index : '$posn'"
- } else {
- return $result
- }
-}
-
-
-#!todo - consider use of 'end-x' syntax for 'to', and implications re consistency with other commands.
-$COL .. PatternMethod setPosn {idx to} {
- var o_data
-
- if {![string is integer -strict $to]} {
- error "destination position must be numeric, consider reKey method if you are trying to change the string key under which this value is stored"
- }
-
- if {[string is integer -strict $idx]} {
- set idx [expr {$idx % [dict size $o_data]}]
-
- set key [lindex [dict keys $o_data] $idx]
- set posn $idx
- } else {
- set key $idx
- set posn [lsearch -exact [dict keys $o_data] $key]
- }
-
- set to [expr {$to % [dict size $o_data]}]
-
-
- set val [dict get $o_data $key]
- dict unset o_data $key
-
- #treat dict as list
- set o_data [linsert $o_data[set o_data {}] [expr {$posn*2}] $key $val]
-
- #set o_list [lreplace $o_list $posn $posn]
- #set o_list [linsert $o_list $to $key]
-
- return $to
-}
-#!todo - improve efficiency of calls to other functions on this object.. 'inline'??
-#presumably the collection object functionality will be long-term stable because it's purpose is to be a core datastructure; therefore it should be reasonable to favour efficiency over maintainability.
-$COL .. PatternMethod incrPosn {idx {by 1}} {
- var o_data
- if {[string is integer -strict $idx]} {
- set idx [expr {$idx % [dict size $o_data]}]
- set key [lindex [dict keys $o_data] $idx]
- set posn $idx
- } else {
- set key $idx
- set posn [lsearch -exact [dict keys $o_data] $key]
- }
-
- set newPosn [expr {($posn + $by) % [dict size $o_data]}]
-
- setPosn $_ID_ $posn $newPosn
- return $newPosn
-}
-$COL .. PatternMethod decrPosn {idx {by 1}} {
- var
- return [incrPosn $_ID_ $idx [expr {- $by}]]
-}
-$COL .. PatternMethod move {idx to} {
- var
- return [setPosn $_ID_ $idx $to]
-}
-$COL .. PatternMethod posn {key} {
- var o_data
- return [lsearch -exact [dict keys $o_data] $key]
-}
-
-#!todo? - disallow numeric values for newKey so as to be consistent with add
-#!note! - item can be reKeyed out from under an alias such that the alias chain no longer points to anything
-# - this is ok.
-$COL .. PatternMethod reKey {idx newKey} {
- var o_data o_alias
-
-
- if {[dict exists $o_data $newKey]} {
- #puts stderr "==============> reKey collision, key $newKey already exists in this collection"
- error "reKey collision, key '$newKey' already exists in this collection"
- }
- if {[info exists o_alias($newKey)]} {
- if {[dict exists $o_data $o_alias($newKey)]} {
- error "reKey collision, key '$newKey' already present as an alias in this collection"
- } else {
- set newKey $o_alias($newKey)
- }
- }
-
-
-
- if {[string is integer -strict $idx]} {
- if {$idx < 0} {
- set idx "end-[expr {abs($idx + 1)}]"
- }
- set key [lindex [dict keys $o_data] $idx]
- set posn $idx
- } else {
- set key $idx
- set posn [lsearch -exact [dict keys $o_data] $key]
- if {$posn < 0} {
- if {[catch {set o_alias($key)} nextKey]} {
- error "no such index: '$idx'"
- } else {
- #try with next key in alias chain...
- #return [reKey $_ID_ $nextKey $newKey]
- tailcall reKey $_ID_ $nextKey $newKey
- }
- }
- }
-
- #set o_list [lreplace $o_list $posn $posn $newKey]
- ##atomic? (traces on array?)
- #set o_array($newKey) $o_array($key)
- #unset o_array($key)
-
- dict set o_data $newKey [dict get $o_data $key]
- dict unset o_data $key
-
- return
-}
-$COL .. PatternMethod hasKey {key} {
- var o_data
- dict exists $o_data $key
-}
-$COL .. PatternMethod hasAlias {key} {
- var o_alias
- info exists o_alias($key)
-}
-
-#either key or alias
-$COL .. PatternMethod hasIndex {key} {
- var o_data o_alias
- if {[dict exists $o_data $key]} {
- return 1
- } else {
- return [info exists o_alias($key)]
- }
-}
-
-
-#Shuffle methods from http://mini.net/tcl/941
-$COL .. PatternMethod shuffleFast {} {
- #shuffle6 - fast, but some orders more likely than others.
-
- var o_data
-
- set keys [dict keys $o_data]
-
- set n [llength $keys]
- for { set i 1 } { $i < $n } { incr i } {
- set j [expr { int( rand() * $n ) }]
- set temp [lindex $keys $i]
- lset keys $i [lindex $keys $j]
- lset keys $j $temp
- }
-
- #rebuild dict in new order
- #!todo - can we do the above 'in place'?
- set newdata [dict create]
- foreach k $keys {
- dict set newdata $k [dict get $o_data $k]
- }
- set o_data $newdata
-
- return
-}
-$COL .. PatternMethod shuffle {} {
- #shuffle5a
-
- var o_data
-
- set n 1
- set keys [list] ;#sorted list of keys
- foreach k [dict keys $o_data] {
- #set index [expr {int(rand()*$n)}]
-
- #set slist [linsert [::pattern::K $keys [set keys {}]] $index $k]
-
- #faster alternative.. 'inline K' [lindex [list a b] 0] ~ [K a b]
- set keys [linsert [lindex [list $keys [set keys {}]] 0] [expr {int(rand()*$n)}] $k]
- incr n
- }
-
- #rebuild dict in new order
- #!todo - can we do the above 'in place'?
- set newdata [dict create]
- foreach k $keys {
- dict set newdata $k [dict get $o_data $k]
- }
- set o_data $newdata
-
- return
-}
-
-
-#search is a somewhat specialised form of 'itemKeys'
-$COL .. PatternMethod search {value args} {
- var o_data
- #only search on values as it's possible for keys to match - especially with options such as -glob
- set matches [lsearch {*}$args [dict values $o_data] $value]
-
- if {"-inline" in $args} {
- return $matches
- } else {
- set keylist [list]
- foreach i $matches {
- set idx [expr {(($i + 1) * 2) -2}]
- lappend keylist [lindex $o_data $idx]
- }
- return $keylist
- }
-}
-
-#inverse lookup
-$COL .. PatternMethod itemKeys {value} {
- var o_data
- #only search on values as it's possible for keys to match
- set value_indices [lsearch -all [dict values $o_data] $value]
-
- set keylist [list]
- foreach i $value_indices {
- set idx [expr {(($i + 1) * 2) -2}]
- lappend keylist [lindex $o_data $idx]
- }
- return $keylist
-}
-
-#invert:
-#change collection to be indexed by its values with the old keys as new values.
-# - keys of duplicate values become a list keyed on the value.
-#e.g the array equivalent is:
-# arr(a) 1
-# arr(b) 2
-# arr(c) 2
-#becomes
-# inv(1) a
-# inv(2) {b c}
-#where the order of duplicate-value keys is not defined.
-#
-#As the total number of keys may change on inversion - order is not preserved if there are ANY duplicates.
-#
-
-
-#!todo - try just [lreverse $o_data] ??
-
-
-$COL .. PatternMethod invert {{splitvalues ""}} {
-
- var o_data o_count o_dupes o_inverted
-
-
- if {$splitvalues eq ""} {
- #not overridden - use o_dupes from last call to determine if values are actually keylists.
- if {$o_dupes > 0} {
- set splitvalues 1
- } else {
- set splitvalues 0
- }
- }
-
-
- #set data [array get o_array]
- set data $o_data
-
- if {$o_count > 500} {
- #an arbitrary optimisation for 'larger' collections.
- #- should theoretically keep the data size and save some reallocations.
- #!todo - test & review
- #
- foreach nm [dict keys $o_data] {
- dict unset o_data $nm
- }
- } else {
- set o_data [dict create]
- }
-
- if {!$splitvalues} {
- dict for {k v} $data {
- dict set o_data $v $k
- }
- } else {
- dict for {k v} $data {
- #we're splitting values because each value is a list of keys
- #therefore sub should be unique - no need for lappend in this branch.
- foreach sub $v {
- #if {[info exists o_array($sub)]} {
- # puts stderr "---here! v:$v sub:$sub k:$k"
- # lappend o_array($sub) $k
- #} else {
- dict set o_data $sub $k
- #}
- }
- }
- }
-
-
- if {[dict size $o_data] != $o_count} {
- #must have been some dupes
-
- set o_dupes [expr {$o_count - [dict size $o_data]}]
- #update count to match inverted collection
- set o_count [dict size $o_data]
- } else {
- set o_dupes 0
- }
-
- set o_inverted [expr {!$o_inverted}]
-
- #'dupes' is the size difference - so 3 equal values in the original collection corresponds to '2 dupes'
- return $o_dupes
-}
-
-
-
-
-
-
-#NOTE: values are treated as lists and split into separate keys for inversion only if requested!
-# To treat values as keylists - set splitvalues 1
-# To treat each value atomically - set splitvalues 0
-# i.e only set splitvalues 1 if you know the values represent duplicate keys from a previous call to invert!
-#
-#
-#Initially call invert with splitvalues = 0
-#To keep calling invert and get back where you started..
-# The rule is... if the previous call to invert returned > 0... pass 1 on the next call.
-#
-$COL .. PatternMethod invert_manual {{splitvalues 0}} {
- #NOTE - the list nesting here is *tricky* - It probably isn't broken.
-
- var o_list o_array o_count
-
- set data [array get o_array]
-
- if {$o_count > 500} {
- #an arbitrary optimisation for 'large' collections.
- #- should theoretically keep the array size and save some reallocations.
- #!todo - test & review
- #
- foreach nm [array names o_array] {
- unset o_array($nm)
- }
- } else {
- array unset o_array
- }
-
- if {!$splitvalues} {
- foreach {k v} $data {
- lappend o_array($v) $k
- }
- } else {
- foreach {k v} $data {
- #we're splitting values because each value is a list of keys
- #therefore sub should be unique - no need for lappend in this branch.
- foreach sub $v {
- #if {[info exists o_array($sub)]} {
- # puts stderr "---here! v:$v sub:$sub k:$k"
- # lappend o_array($sub) $k
- #} else {
- set o_array($sub) $k
- #}
- }
- }
- }
-
-
- if {[array size o_array] != $o_count} {
- #must have been some dupes
- set o_list [array names o_array]
-
-
- set dupes [expr {$o_count - [array size o_array]}]
- #update count to match inverted collection
- set o_count [array size o_array]
- } else {
- #review - are these machinations worthwhile for order preservation? what speed penalty do we pay?
- array set prev $data
- set i -1
- if {$splitvalues} {
- #values are lists of length one. Take lindex 0 so list values aren't overnested.
- foreach oldkey $o_list {
- lset o_list [incr i] [lindex $prev($oldkey) 0]
- }
- } else {
- foreach oldkey $o_list {
- lset o_list [incr i] $prev($oldkey)
- }
- }
-
- set dupes 0
- }
-
-
- #'dupes' is the size difference - so 3 equal values in the original collection corresponds to '2 dupes'
- return $dupes
-}
-
-
-
-#Note that collections cannot be inverted without loss of information if they have duplicates AND compound keys
-# (keys that are lists)
-$COL .. PatternMethod invert_lossy {{splitvalues 1}} {
- var o_list o_array o_count
-
- set data [array get o_array]
-
- if {$o_count > 500} {
- #an arbitrary optimisation for 'large' collections.
- #- should theoretically keep the array size and save some reallocations.
- #!todo - test & review
- #
- foreach nm [array names o_array] {
- unset o_array($nm)
- }
- } else {
- array unset o_array
- }
-
- if {!$splitvalues} {
- foreach {k v} $data {
- #note! we must check for existence and use 'set' for first case.
- #using 'lappend' only will result in deeper nestings on each invert!
- #If you don't understand this - don't change it!
- if {[info exists o_array($v)]} {
- lappend o_array($v) $k
- } else {
- set o_array($v) $k
- }
- }
- } else {
- foreach {k v} $data {
- #length test necessary to avoid incorrect 'un-nesting'
- #if {[llength $v] > 1} {
- foreach sub $v {
- if {[info exists o_array($sub)]} {
- lappend o_array($sub) $k
- } else {
- set o_array($sub) $k
- }
- }
- #} else {
- # if {[info exists o_array($v)]} {
- # lappend o_array($v) $k
- # } else {
- # set o_array($v) $k
- # }
- #}
- }
- }
-
-
- if {[array size o_array] != $o_count} {
- #must have been some dupes
- set o_list [array names o_array]
-
-
- set dupes [expr {$o_count - [array size o_array]}]
- #update count to match inverted collection
- set o_count [array size o_array]
- } else {
- #review - are these machinations worthwhile for order preservation? what speed penalty do we pay?
- array set prev $data
- set i -1
- foreach oldkey $o_list {
- lset o_list [incr i] $prev($oldkey)
- }
- set dupes 0
- }
-
-
- #'dupes' is the size difference - so 3 equal values in the original collection corresponds to '2 dupes'
- return $dupes
-}
-
-$COL .. PatternMethod reverse {} {
- var o_data
-
- set dictnew [dict create]
- foreach k [lreverse [dict keys $o_data]] {
- dict set dictnew $k [dict get $o_data $k]
- }
- set o_data $dictnew
- return
-}
-
-$COL .. PatternMethod keySort {{options -ascii}} {
- var o_data
-
- set keys [lsort {*}$options [dict keys $o_data]]
-
- set dictnew [dict create]
- foreach k $keys {
- dict set dictnew $k [dict get $o_data $k]
- }
- set o_data $dictnew
-
- return
-}
-
-#!todo - allow simple options in combination with options such as -command and -object. Redo args handling completely for more complex sorting.
-$COL .. PatternMethod sort {args} {
- var o_data
-
- #defaults
- set options [dict create -index 1] ;#values always in subelement 1 of name-value pair list for sorting.
-
- set options_simple [list]
-
-
- for {set i 0} {$i < [llength $args]} {incr i} {
- set a [lindex $args $i]
- switch -- $a {
- -indices -
- -ascii -
- -dictionary -
- -integer -
- -real -
- -increasing -
- -decreasing {
- #dict set options $a 1
- lappend options_simple $a
- }
- -unique {
- #not a valid option
- #this would stuff up the data...
- #!todo? - remove dups from collection if this option used? - alias the keys?
- }
- -object {
- #!todo - treat value as object and allow sorting by sub-values .eg >col1 . sort -object ". sub . property" -increasing
- #may be slow - but handy. Consider -indexed property to store/cache these values on first run
- }
- -command {
- dict set options $a [lindex $args [incr i]]
- }
- -index {
- #allow sorting on subindices of the value.
- dict set options -index [concat [dict get $options -index] [lindex $args [incr i]] ]
- }
- default {
- #unrecognised option - print usage?
- }
- }
- }
-
-
-
- if {[set posn [lsearch -exact $options_simple "-indices"]] >= 0} {
-
- var o_array
-
- set slist [list]
- foreach k [dict keys $o_data] {
- lappend slist [list $k [dict get $o_data $k]]
- }
- return [lsort {*}$options_simple {*}$options $slist]
-
-
-
- #set options_simple [lreplace $options_simple $posn $posn] ;#
- #set slist [list]
- #foreach {n v} [array get ::p::[lindex ${_ID_} 0 0]::o_array] {
- # lappend slist [list $n $v]
- #}
- #set slist [lsort {*}$options_simple {*}$options $slist]
- #foreach i $slist {
- # #determine the position in the collections list
- # lappend result {*}[lsearch -exact $o_list [lindex $i 0]]
- #}
- #return $result
- } else {
- set slist [list]
- dict for {k v} $o_data {
- lappend slist [list $k $v]
- }
- #set slist [lsort {*}$options_simple {*}$options $slist]
- set slist [lsort {*}$options_simple {*}$options $slist[set slist {}]] ;#K combinator for efficiency
-
-
- #set o_list [lsearch -all -inline -subindices -index 0 $slist *]
-
- set o_data [dict create]
- foreach pair $slist {
- dict set o_data [lindex $pair 0] [lindex $pair 1]
- }
-
-
-
- return
- }
-
-}
-
-
-$COL .. PatternMethod clear {} {
- var o_data o_count
-
- set o_data [dict create]
- set o_count 0
- #aliases?
- return
-}
-
-#see http://wiki.tcl.tk/15271 - A generic collection traversal interface
-#
-#!todo - options: -progresscommand -errorcommand (-granularity ?) (-self ? (to convert to an iterator?))
-#!todo? - lazy retrieval of items so that all changes to the collection are available to a running asynch enumeration?
-# - should this be an option? which mechanism should be the default?
-# - currently only the keylist is treated in 'snapshot' fashion
-# so values could be changed and the state could be invalidated by other code during an enumeration
-#
-$COL .. PatternMethod enumerate {args} {
- #----------
- lassign [lrange $args end-1 end] cmd seed
- set optionlist [list]
- foreach a [lrange $args 0 end-2] {
- lappend optionlist $a
- }
- set opt(-direction) left
- set opt(-completioncommand) ""
- array set opt $optionlist
- #----------
- var o_data
-
- if {[string tolower [string index $opt(-direction) 0]] eq "r"} {
- #'right' 'RIGHT' 'r' etc.
- set list [lreverse [dict keys $o_data]]
- } else {
- #normal left-right order
- set list [dict keys $o_data]
- }
-
- if {![string length $opt(-completioncommand)]} {
- #standard synchronous processing
- foreach k $list {
- set seed [uplevel #0 [list {*}$cmd $seed [dict get $o_data $k]]]
- }
- return $seed
- } else {
- #ASYNCHRONOUS enumeration
- var this o_bgEnum
- #!todo - make id unique
- #!todo - facility to abort running enumeration.
- set enumID enum[array size o_bgEnum]
-
- set seedvar [$this . bgEnum $enumID .]
- set $seedvar $seed
-
- after 0 [list $this . _doBackgroundEnum $enumID $list $cmd $seedvar $opt(-completioncommand)]
- return $enumID
- }
-}
-
-#!todo - make private? - put on a separate interface?
-$COL .. PatternMethod _doBackgroundEnum {enumID slice cmd seedvar completioncommand} {
- var this o_data
-
-
- #Note that we don't post to the eventqueue using 'foreach s $slice'
- # we only schedule another event after each item is processed
- # - otherwise we would be spamming the eventqueue with items.
-
- #!todo? - accept a -granularity option to allow handling of n list-items per event?
-
- if {[llength $slice]} {
- set slice [lassign $slice head]
-
- set script [string map [list %cmd% $cmd %seedvar% $seedvar %val% [dict get $o_data $head]] {
- %cmd% [set %seedvar%] %val%
- }]
-
- #post to eventqueue and re-enter _doBackgroundEnum
- #
- after idle [list after 0 [subst {set $seedvar \[uplevel #0 [list $script] \]; $this . _doBackgroundEnum $enumID [list $slice] [list $cmd] $seedvar [list $completioncommand]}]]
-
- } else {
- #done.
-
- set script [string map [list %cmd% $completioncommand %seedvar% $seedvar] {
- lindex [list [%cmd% [set %seedvar%]] [unset %seedvar%]] 0
- }]
-
- after idle [list after 0 [list uplevel #0 $script]]
- }
-
- return
-}
-
-$COL .. PatternMethod enumeratorstate {} {
- var o_bgEnum
- parray o_bgEnum
-}
-
-#proc ::bgerror {args} {
-# puts stderr "=bgerror===>$args"
-#}
-
-
-#map could be done in terms of the generic 'enumerate' method.. but it's slower.
-#
-#$PM map2 {proc} {
-# var
-# enumerate $_ID_ [list ::map-helper $proc] [list]
-#}
-#proc ::map-helper {proc accum item} {
-# lappend accum [uplevel #0 [list {*}$proc $item]]
-#}
-
-$COL .. PatternMethod map {cmd} {
- var o_data
- set seed [list]
- dict for {k v} $o_data {
- lappend seed [uplevel #0 [list {*}$cmd $v]]
- }
-
- return $seed
-}
-$COL .. PatternMethod objectmap {cmd} {
- var o_data
- set seed [list]
- dict for {k v} $o_data {
- lappend seed [uplevel #0 [list $v {*}$cmd]]
- }
-
- return $seed
-}
-
-
-#End core collection functionality.
-#collection 'mixin' interfaces
-
->pattern .. Create >keyvalprotector
->keyvalprotector .. PatternVariable o_protectedkeys
->keyvalprotector .. PatternVariable o_protectedvals
-
-#!todo - write test regarding errors in Constructors for mixins like this
-# - an error (e.g from bad args) can cause errors with vars after it's re-run with correct args
->keyvalprotector .. Constructor {args} {
- var this o_protectedkeys o_protectedvals
- set this @this@
- #----------------------------------------------------------------------------
- set known_opts [list -keys -vals ]
- dict set default -keys [list]
- dict set default -vals [list]
- if {([llength $args] % 2) != 0} {
- error "(>keyvalprotector .. Constructor) ERROR: uneven options supplied - must be of form '-option value' "
- }
- foreach {k v} $args {
- if {$k ni $known_opts} {
- error "(>keyvalprotector .. Constructor) ERROR: option '$k' not in known options: '$known_opts'"
- }
- }
- set opts [dict merge $default $args]
- set o_protectedkeys [dict get $opts -keys]
- set o_protectedvals [dict get $opts -vals]
- #----------------------------------------------------------------------------
- set protections [concat $o_protectedkeys $o_protectedvals]
- if {![llength $protections]} {
- error "(>keyvalprotector .. Constructor) ERROR: must supply at least one argument to -vals or -keys"
- }
-
-}
->keyvalprotector .. PatternMethod clear {} {
- error "(>keyvalprotector . clear) ERROR: This collection is protected by a >keyvalprotector mixin. Cannot clear"
-}
->keyvalprotector .. PatternMethod pop {{idx ""}} {
- var o_data o_count o_protectedkeys o_protectedvals
-
- if {$idx eq ""} {
- set key [lindex [dict keys $o_data] end]
- } else {
- if {[string is integer -strict $idx]} {
- set key [lindex [dict keys $o_data] $idx]
- } else {
- set key $idx
- }
- }
-
- if {$key in $o_protectedkeys} {
- error "(>keyvalprotector . pop) ERROR: Cannot pop object with index '$idx', key '$key' from collection."
- }
- set posn [lsearch -exact [dict keys $o_data] $key]
- if {($posn >= 0) && ($posn < [dict size $o_data])} {
- set result [dict get $o_data $key]
- if {$result in $o_protectedvals} {
- error "(>keyvalprotector . pop) ERROR: Cannot pop object '$result' with index '$idx', key '$key' from collection."
- }
- dict unset o_data $key
- set o_count [dict size $o_data]
- return $result
- } else {
- error "no such index: '$idx'"
- }
-
-}
->keyvalprotector .. PatternMethod remove {idx {endRange ""}} {
- var this o_data o_count o_alias o_protectedkeys o_protectedvals
-
- if {[string length $endRange]} {
- error "ranged removal not yet implemented.. remove one item at a time."
- }
-
- if {[string is integer -strict $idx]} {
- if {$idx < 0} {
- set idx "end-[expr {abs($idx + 1)}]"
- }
- set key [lindex [dict keys $o_data] $idx]
- if {$key in $o_protectedkeys} {
- error "(>keyvalprotector . remove) ERROR: cannot remove item with index '$idx' key '$key' from collection"
- }
- set posn $idx
- } else {
- set key $idx
- set posn [lsearch -exact [dict keys $o_data] $key]
- if {$posn < 0} {
- if {[catch {set o_alias($key)} nextKey]} {
- error "no such index: '$idx' in collection: $this"
- } else {
- if {$key in $o_protectedkeys} {
- error "(>keyvalprotector . remove) ERROR: cannot remove item with index '$idx' from collection"
- }
- #try with next key in alias chain...
- #return [remove $_ID_ $nextKey]
- tailcall remove $_ID_ $nextKey
- }
- }
- }
-
- dict unset o_data $key
-
- set o_count [dict size $o_data]
- return
-}
-
-#1)
-#predicate methods (order preserving)
-#usage:
-# >collection .. Create >c1
-# >predicatedCollection .. Create >c1 ;#overlay predicate methods on existing collection
-
-#e.g >col1 . all {$val > 14}
-#e.g >col1 . filterToCollection {$val > 19} . count
-#e.g >col1 . filter {[string match "x*" $key]}
-#!todo - fix. currying fails..
-
-::>pattern .. Create >predicatedCollection
-#process_pattern_aliases ::patternlib::>predicatedCollection
-
-set PM [>predicatedCollection .. PatternMethod .]
-
->predicatedCollection .. PatternMethod filter {predicate} {
- var this o_list o_array
- set result [list]
-
- #!note (jmn 2004) how could we do smart filtering based on $posn?
- #i.e it would make sense to lrange $o_list based on $posn...
- #but what about complicated expressions where $posn is a set of ranges and/or combined with tests on $key & $val ??
- #Seems better to provide an alternative efficient means of generating subcolllections/ranges to perform predicate operations upon.
- #given this, is $posn even useful?
-
- set posn 0
- foreach key $o_list {
- set val $o_array($key)
- if $predicate {
- lappend result $val
- }
- incr posn
- }
- set result
-}
->predicatedCollection .. PatternMethod filterToKeys {predicate} {
- var this o_list o_array
- set result [list]
-
- set posn 0
- foreach key $o_list {
- set val $o_array($key)
- if $predicate {
- lappend result $key
- }
- incr posn
- }
- set result
-}
->predicatedCollection .. PatternMethod filterToCollection {predicate {destCollection {}}} {
- #!todo - collection not in subordinate namespace? -> if subordinate, should imply modification of sub's contents will be reflected in parent?
- #!todo - implement as 'view' on current collection object.. extra o_list variables?
- #!todo - review/document 'expected' key collision behaviour - source keys used as dest keys.. -autokey option required?
- var this o_list o_array m_i_filteredCollection
-
- incr m_i_filteredCollection
- if {![string length $destCollection]} {
- #!todo? - implement 'one-shot' object (similar to RaTcl)
- set result [::patternlib::>collection .. Create [$this .. Namespace]::>filteredCollection-$m_i_filteredCollection]
- } else {
- set result $destCollection
- }
-
- ####
- #externally manipulate new collection
- #set ADD [$c . add .]
- #foreach key $o_list {
- # set val $o_array($key)
- # if $predicate {
- # $ADD $val $key
- # }
- #}
- ###
-
- #internal manipulation faster
- #set cID [lindex [set $result] 0]
- set cID [lindex [$result --] 0]
-
- #use list to get keys so as to preserve order
- set posn 0
- upvar #0 ::p::${cID}::o_array cARRAY ::p::${cID}::o_list cLIST
- foreach key $o_list {
- set val $o_array($key)
- if $predicate {
- if {[info exists cARRAY($key)]} {
- error "key '$key' already exists in this collection"
- }
- lappend cLIST $key
- set cARRAY($key) $val
- }
- incr posn
- }
-
- return $result
-}
-
-#NOTE! unbraced expr/if statements. We want to evaluate the predicate.
->predicatedCollection .. PatternMethod any {predicate} {
- var this o_list o_array
- set posn 0
- foreach key $o_list {
- set val $o_array($key)
- if $predicate {
- return 1
- }
- incr posn
- }
- return 0
-}
->predicatedCollection .. PatternMethod all {predicate} {
- var this o_list o_array
- set posn 0
- foreach key $o_list {
- set val $o_array($key)
- if !($predicate) {
- return 0
- }
- incr posn
- }
- return 1
-}
->predicatedCollection .. PatternMethod dropWhile {predicate} {
- var this o_list o_array
- set result [list]
- set _idx 0
- set posn 0
- foreach key $o_list {
- set val $o_array($key)
- if $predicate {
- incr _idx
- } else {
- break
- }
- incr posn
- }
- set remaining [lrange $o_list $_idx end]
- foreach key $remaining {
- set val $o_array($key)
- lappend result $val
- }
- return $result
-}
->predicatedCollection .. PatternMethod takeWhile {predicate} {
- var this o_list o_array
- set result [list]
- set posn 0
- foreach key $o_list {
- set val $o_array($key)
- if $predicate {
- lappend result $val
- } else {
- break
- }
- incr posn
- }
- set result
-}
-
-
-
-#end >collection mixins
-######################################
-
-
-
-
-#-----------------------------------------------------------
-#!TODO - methods for converting an arrayHandle to & from a hashMap efficiently?
-# Why do we need both? apart from the size variable, what is the use of hashMap?
-#-----------------------------------------------------------
-#::pattern::create >hashMap
-::>pattern .. Create >hashMap
-
->hashMap .. PatternVariable o_size
->hashMap .. PatternVariable o_array
-
->hashMap .. Constructor {args} {
- var o_array o_size
- array set o_array [list]
- set o_size 0
-}
->hashMap .. PatternDefaultMethod "item"
->hashMap .. PatternMethod item {key} {
- var o_array
- set o_array($key)
-}
->hashMap .. PatternMethod items {} {
- var o_array
-
- set result [list]
- foreach nm [array names o_array] {
- lappend result $o_array($nm)
- }
- return $result
-}
->hashMap .. PatternMethod pairs {} {
- var o_array
-
- array get o_array
-}
->hashMap .. PatternMethod add {val key} {
- var o_array o_size
-
- set o_array($key) $val
- incr o_size
- return $key
-}
-
->hashMap .. PatternMethod del {key} {
- var
- puts stderr "warning: 'del' method of >hashMap deprecated. Use 'remove' instead."
- remove $_ID_ $key
-}
->hashMap .. PatternMethod remove {key} {
- var o_array o_size
- unset o_array($key)
- incr o_size -1
- return $key
-}
->hashMap .. PatternMethod count {} {
- var o_size
- #array size o_array
- return $o_size
-}
->hashMap .. PatternMethod count2 {} {
- var o_array
- #array size o_array ;#slow, at least for TCLv8.4.4
- #even array statistics is faster than array size !
- #e.g return [lindex [array statistics o_array] 0]
- #but.. apparently there are circumstances where array statistics doesn't report the correct size.
- return [array size o_array]
-}
->hashMap .. PatternMethod names {} {
- var o_array
- array names o_array
-}
->hashMap .. PatternMethod keys {} {
- #synonym for names
- var o_array
- array names o_array
-}
->hashMap .. PatternMethod hasKey {key} {
- var o_array
- return [info exists o_array($key)]
-}
->hashMap .. PatternMethod clear {} {
- var o_array o_size
- unset o_array
- set o_size 0
- return
-}
-#>hashMap .. Ready 1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-#explicitly create metadata. Not required for user-defined patterns.
-# this is only done here because this object is used for the metadata of all objects
-# so the object must have all it's methods/props before its own metadata structure can be built.
-#uplevel 1 "::pattern::object ::pattern::>_nullMeta createMetadata >collection"
-#uplevel 1 "::patternlib::>collection .. CreateMetadata ::patternlib::>collection"
-
-
-
-
-if 0 {
-
-
-#-----------------------------------------------------------
-#::pattern::create >arrayHandle {
-# variable o_arrayName
-# variable this
-#}
-::>pattern .. Create >arrayHandle
-
->arrayHandle .. PatternVariable o_arrayName
->arrayHandle .. PatternVariable this
-
->arrayHandle .. Constructor {args} {
- var o_arrayName this
- set this @this@
-
-
- set o_arrayName [$this .. Namespace]::array
-
- upvar #0 $o_arrayName $this
- #? how to automatically update this after a namespace import?
-
- array set $o_arrayName [list]
-
-}
->arrayHandle .. PatternMethod array {} {
- var o_arrayName
- return $o_arrayName
-}
-
-#-------------------------------------------------------
-#---- some experiments
->arrayHandle .. PatternMethod up {varname} {
- var o_arrayName
-
- #is it dodgy to hard-code the calling depth?
- #will it be different for different object systems?
- #Will it even be consistent for the same object.
- # Is this method necessary anyway? -
- # - users can always instead do:
- # upvar #0 [>instance . array] var
-
- uplevel 3 [list upvar 0 $o_arrayName $varname]
-
- return
-}
->arrayHandle .. PatternMethod global {varname} {
- var o_arrayName
- # upvar #0 [>instance . array] var
-
- if {![string match ::* $varname]} {
- set varname ::$varname
- }
-
- upvar #0 $o_arrayName $varname
-
- return
-}
->arrayHandle .. PatternMethod depth {} {
- var o_arrayName
- #
- for {set i 0} {$i < [info level]} {
- puts "${i}: [uplevel $i [list namespace current] , [info level $i]]"
- }
-
-}
- # --------------------------------------------
-
-
->arrayHandle .. PatternMethod item {key} {
- var o_arrayName
- set ${o_arrayName}($key)
-}
->arrayHandle .. PatternMethod items {} {
- var o_arrayName
-
- set result [list]
- foreach nm [array names $o_arrayName] {
- lappend result [set ${o_arrayName}($nm)]
- }
- return $result
-}
->arrayHandle .. PatternMethod pairs {} {
- var o_arrayName
-
- array get $o_arrayName
-}
->arrayHandle .. PatternMethod add {val key} {
- var o_arrayName
-
- set ${o_arrayName}($key) $val
- return $key
-}
->arrayHandle .. PatternMethod del {key} {
- puts stderr "Warning: 'del' method of >arrayHandle deprecated. Use 'remove' instead."
- remove $_ID_ $key
-}
->arrayHandle .. PatternMethod remove {key} {
- var o_arrayName
- unset ${o_arrayName}($key)
- return $key
-}
->arrayHandle .. PatternMethod size {} {
- var o_arrayName
- return [array size $o_arrayName]
-}
->arrayHandle .. PatternMethod count {} {
- #alias for size
- var o_arrayName
- return [array size $o_arrayName]
-}
->arrayHandle .. PatternMethod statistics {} {
- var o_arrayName
- return [array statistics $o_arrayName]
-}
->arrayHandle .. PatternMethod names {} {
- var o_arrayName
- array names $o_arrayName
-}
->arrayHandle .. PatternMethod keys {} {
- #synonym for names
- var o_arrayName
- array names $o_arrayName
-}
->arrayHandle .. PatternMethod hasKey {key} {
- var o_arrayName
-
- return [info exists ${o_arrayName}($key)]
-}
->arrayHandle .. PatternMethod clear {} {
- var o_arrayName
- unset $o_arrayName
- array set $o_arrayName [list]
-
- return
-}
-#>arrayHandle .. Ready 1
-
-
-
-
-::>pattern .. Create >matrix
-
->matrix .. PatternVariable o_array
->matrix .. PatternVariable o_size
-
->matrix .. Constructor {args} {
- var o_array o_size
-
- array set o_array [list]
- set o_size 0
-}
-
-
-#process_pattern_aliases ::patternlib::>matrix
-
-set PM [>matrix .. PatternMethod .]
-
->matrix .. PatternMethod item {args} {
- var o_array
-
- if {![llength $args]} {
- error "indices required"
- } else {
-
- }
- if [info exists o_array($args)] {
- return $o_array($args)
- } else {
- error "no such index: '$args'"
- }
-}
->matrix .. PatternMethod items {} {
- var o_array
-
- set result [list]
- foreach nm [array names o_array] {
- lappend result $o_array($nm)
- }
- return $result
-}
->matrix .. PatternMethod pairs {} {
- var o_array
-
- array get o_array
-}
->matrix .. PatternMethod slice {args} {
- var o_array
-
- if {"*" ni $args} {
- lappend args *
- }
-
- array get o_array $args
-}
->matrix .. PatternMethod add {val args} {
- var o_array o_size
-
- if {![llength $args]} {
- error "indices required"
- }
-
- set o_array($args) $val
- incr o_size
-
- #return [array size o_array]
- return $o_size
-}
->matrix .. PatternMethod names {} {
- var o_array
- array names o_array
-}
->matrix .. PatternMethod keys {} {
- #synonym for names
- var o_array
- array names o_array
-}
->matrix .. PatternMethod hasKey {args} {
- var o_array
-
- return [info exists o_array($args)]
-}
->matrix .. PatternMethod clear {} {
- var o_array o_size
- unset o_array
- set o_size 0
- return
-}
->matrix .. PatternMethod count {} {
- var o_size
- return $o_size
-}
->matrix .. PatternMethod count2 {} {
- var o_array
- #see comments for >hashMap count2
- return [array size o_array]
-}
-#>matrix .. Ready 1
-
-#--------------------------------------------------------
-#tree data structure (based *loosely* on API at http://www.msen.com/%7Eclif/treeNobj.html - discussed in Clif Flynts book Tcl programming)
-#!todo - compare API to http://tcllib.sourceforge.net/doc/tree.html
-#!todo - create an >itree (inheritance tree) where node data is readable/writable on children unless overridden.
-::>pattern .. Create >tree
-
-set _NODE [::>pattern .. Create [>tree .. Namespace]::>node]
-set _TREE_NODE $_NODE
-#process_pattern_aliases $_TREE_NODE
-
-$_NODE .. PatternVariable o_treens ;#tree namespace
-$_NODE .. PatternVariable o_idref
-$_NODE .. PatternVariable o_nodePrototype
-
-#$_NODE .. PatternProperty data
-$_NODE .. PatternProperty info
-
-$_NODE .. PatternProperty tree
-$_NODE .. PatternProperty parent
-$_NODE .. PatternProperty children
-$_NODE .. PatternMethod addNode {} {
- set nd_id [incr $o_idref]
- set nd [$o_nodePrototype .. Create ${o_treens}::>n-$nd_id -tree $o_tree -parent @this@]
- @this@ . add $nd n-$nd_id
-
- return n-$nd_id
-}
-#flat list of all nodes below this
-#!todo - something else? ad-hoc collections?
-#!todo - non-recursive version? tail-call opt?
-$_NODE .. PatternMethod nodes {} {
- set result [list]
-
- #use(abuse?) our knowledge of >collection internals
- foreach n $o_list {
- #eval lappend result $n [$o_array($n) . nodes]
- #!todo - test
- lappend result $n {*}[$o_array($n) . nodes]
- }
- return $result
-}
-#count of number of descendants
-#!todo - non-recursive version? tail-call opt?
-$_NODE .. PatternMethod size {} {
- set result 0
- #use(abuse?) our knowledge of >collection internals
- foreach n $o_list {
- incr result [expr {1 + [$o_array($n) . size]}]
- }
- return $result
-}
-$_NODE .. PatternMethod isLeaf {} {
- #!todo - way to stop unused vars being uplevelled?
- var o_tree
-
- #tailcall isEmpty $_ID_ ;#fails. because isEmpty is from >collection interface - so different ns?
- tailcall [@this@ . isEmpty .]
-}
-$_NODE .. Constructor {args} {
- array set A $args
-
- set o_tree $A(-tree)
- set o_parent $A(-parent)
-
- #array set o_data [list]
- array set o_info [list]
-
- set o_nodePrototype [::patternlib::>tree .. Namespace]::>node
- set o_idref [$o_tree . nodeID .]
- set o_treens [$o_tree .. Namespace]
- #set o_children [::patternlib::>collection .. Create [@this@ .. Namespace]::>children]
-
- #overlay children collection directly on the node
- set o_children [::patternlib::>collection .. Create @this@]
-
- return
-}
-
->tree .. PatternProperty test blah
->tree .. PatternProperty nodeID 0 ;#public only so node can access.. need 'friend' concept?
->tree .. PatternVariable o_ns
->tree .. Constructor {args} {
- set o_ns [@this@ .. Namespace]
-
- #>tree is itself also a node (root node)
- #overlay new 'root' node onto existing tree, pass tree to constructor
- [::patternlib::>tree .. Namespace]::>node .. Create @this@ -tree @this@ -parent ""
-}
-
-
-
-
-unset _NODE
-
-
-
-
-#--------------------------------------------------------
-#a basic binary search tree experiment
-# - todo - 'scheme' property to change behaviour? e.g balanced tree
-::>pattern .. Create >bst
-#process_pattern_aliases ::patternlib::>bst
->bst .. PatternVariable o_NS ;#namespace
->bst .. PatternVariable o_this ;#namespace
->bst .. PatternVariable o_nodeID
-
->bst .. PatternProperty root ""
->bst .. Constructor {args} {
- set o_this @this@
- set o_NS [$o_this .. Namespace]
- namespace eval ${o_NS}::nodes {}
- puts stdout ">bst constructor"
- set o_nodeID 0
-}
->bst .. PatternMethod insert {key args} {
- set newnode [::patternlib::>bstnode .. Create ${o_NS}::nodes::>n-[incr o_nodeID]]
- set [$newnode . key .] $key
- if {[llength $args]} {
- set [$newnode . value .] $args
- }
- if {![string length $o_root]} {
- set o_root $newnode
- set [$newnode . parent .] $o_this
- } else {
- set ipoint {} ;#insertion point
- set tpoint $o_root ;#test point
- set side {}
- while {[string length $tpoint]} {
- set ipoint $tpoint
- if {[$newnode . key] < [$tpoint . key]} {
- set tpoint [$tpoint . left]
- set side left
- } else {
- set tpoint [$tpoint . right]
- set side right
- }
- }
- set [$newnode . parent .] $ipoint
- set [$ipoint . $side .] $newnode
- }
- return $newnode
-}
->bst .. PatternMethod item {key} {
- if {![string length $o_root]} {
- error "item $key not found"
- } else {
- set tpoint $o_root
- while {[string length $tpoint]} {
- if {[$tpoint . key] eq $key} {
- return $tpoint
- } else {
- if {$key < [$tpoint . key]} {
- set tpoint [$tpoint . left]
- } else {
- set tpoint [$tpoint . right]
- }
- }
- }
- error "item $key not found"
- }
-}
->bst .. PatternMethod inorder-walk {} {
- if {[string length $o_root]} {
- $o_root . inorder-walk
- }
- puts {}
-}
->bst .. PatternMethod view {} {
- array set result [list]
-
- if {[string length $o_root]} {
- array set result [$o_root . view 0 [list]]
- }
-
- foreach depth [lsort [array names result]] {
- puts "$depth: $result($depth)"
- }
-
-}
-::>pattern .. Create >bstnode
-#process_pattern_aliases ::patternlib::>bstnode
->bstnode .. PatternProperty parent
->bstnode .. PatternProperty left ""
->bstnode .. PatternProperty right ""
->bstnode .. PatternProperty key
->bstnode .. PatternProperty value
-
->bstnode .. PatternMethod inorder-walk {} {
- if {[string length $o_left]} {
- $o_left . inorder-walk
- }
-
- puts -nonewline "$o_key "
-
- if {[string length $o_right]} {
- $o_right . inorder-walk
- }
-
- return
-}
->bstnode .. PatternMethod view {depth state} {
- #!todo - show more useful representation of structure
- set lower [incr depth]
-
- if {[string length $o_left]} {
- set state [$o_left . view $lower $state]
- }
-
- if {[string length $o_right]} {
- set state [$o_right . view $lower $state]
- }
-
-
- array set s $state
- lappend s($depth) $o_key
-
- return [array get s]
-}
-
-
-#--------------------------------------------------------
-#::pattern::create ::pattern::>metaObject
-#::pattern::>metaObject PatternProperty methods
-#::pattern::>metaObject PatternProperty properties
-#::pattern::>metaObject PatternProperty PatternMethods
-#::pattern::>metaObject PatternProperty patternProperties
-#::pattern::>metaObject Constructor args {
-# set this @this@
-#
-# set [$this . methods .] [::>collection create [$this namespace]::methods]
-# set [$this . properties .] [::>collection create [$this namespace]::properties]
-# set [$this . PatternMethods .] [::>collection create [$this namespace]::PatternMethods]
-# set [$this . patternProperties .] [::>collection create [$this namespace]::patternProperties]
-#
-#}
-
-
-
- #tidy up
- unset PV
- unset PM
-
-
-
-#--------------------------------------------------------
-::>pattern .. Create >enum
-#process_pattern_aliases ::patternlib::>enum
->enum .. PatternMethod item {{idx 0}} {
- var o_array o_list
-
- if {[string is integer -strict $idx]} {
- if {$idx < 0} {
- set idx "end-[expr {abs($idx + 1)}]"
- }
- if {[catch {set o_array([lindex $o_list $idx])} result]} {
- error "no such index : '$idx'"
- } else {
- return $result
- }
- } else {
- if {[catch {set o_array($idx)} result]} {
- error "no such index: '$idx'"
- } else {
- return $result
- }
- }
-}
-
-
-
-#proc makeenum {type identifiers} {
-# #!!todo - make generated procs import into whatever current system context?
-#
-# upvar #0 wbpbenum_${type}_number a1 wbpbenum_number_${type} a2
-#
-# #obliterate any previous enum for this type
-# catch {unset a1}
-# catch {unset a2}
-#
-# set n 0
-# foreach id $identifiers {
-# set a1($id) $n
-# set a2($n) $id
-# incr n
-# }
-# proc ::${type}_to_number key [string map [list @type@ $type] {
-# upvar #0 wbpbenum_@type@_number ary
-# if {[catch {set ary($key)} num]} {
-# return -code error "unknown @type@ '$key'"
-# }
-# return $num
-# }]
-#
-# proc ::number_to_${type} {number} [string map [list @type@ $type] {
-# upvar #0 wbpbenum_number_@type@ ary
-# if {[catch {set ary($number)} @type@]} {
-# return -code error "no @type@ for '$number'"
-# }
-# return $@type@
-# }]
-#
-# #eval "namespace eval ::sysnexus {namespace export number_to_${type}; namespace export ${type}_to_number}"
-# #eval "namespace eval :: {namespace import -force sysnexus::number_to_${type} sysnexus::${type}_to_number}"
-#}
-#
-#--------------------------------------------------------
-::>pattern .. Create >nest
->nest .. PatternVariable THIS
->nest .. PatternProperty data -autoclone
->nest .. Constructor {args} {
- var o_data
- var THIS
- set THIS @this@
- array set o_data [list]
-}
->nest .. PatternMethod item {args} {
- set THIS @this@
- return [$THIS . data [join $args ,]]
-}
-
-#
-# e.g
-# set [>nest a , b . data c .] blah
-# >nest a , b , c
-#
-# set [>nest w x , y . data z .] etc
-# >nest w x , y , z
-#--------------------------------------------------------
-
-}
-
-}
-
-
-#package require patternlibtemp
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patternpredator2-1.2.4.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patternpredator2-1.2.4.tm
deleted file mode 100644
index 680ea88f..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/patternpredator2-1.2.4.tm
+++ /dev/null
@@ -1,754 +0,0 @@
-package provide patternpredator2 1.2.4
-
-proc ::p::internals::jaws {OID _ID_ args} {
- #puts stderr ">>>(patternpredator2 lib)jaws called with _ID_:$_ID_ args: $args"
- #set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid
-
- yield
- set w 1
-
- set stack [list]
- set wordcount [llength $args]
- set terminals [list . .. , # @ !] ;#tokens which require the current stack to be evaluated first
- set unsupported 0
- set operator ""
- set operator_prev "" ;#used only by argprotect to revert to previous operator
-
-
- if {$OID ne "null"} {
- #!DO NOT use upvar here for MAP! (calling set on a MAP in another iteration/call will overwrite a map for another object!)
- #upvar #0 ::p::${OID}::_meta::map MAP
- set MAP [set ::p::${OID}::_meta::map]
- } else {
- # error "jaws - OID = 'null' ???"
- set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] ] ;#MAP taken from _ID_ will be missing 'interfaces' key
- }
- set invocantdata [dict get $MAP invocantdata]
- lassign $invocantdata OID alias default_method object_command wrapped
-
- set finished_args 0 ;#whether we've completely processed all args in the while loop and therefor don't need to peform the final word processing code
-
- #don't use 'foreach word $args' - we sometimes need to backtrack a little by manipulating $w
- while {$w < $wordcount} {
- set word [lindex $args [expr {$w -1}]]
- #puts stdout "w:$w word:$word stack:$stack"
-
- if {$operator eq "argprotect"} {
- set operator $operator_prev
- lappend stack $word
- incr w
- } else {
- if {[llength $stack]} {
- if {$word in $terminals} {
- set reduction [list 0 $_ID_ {*}$stack ]
- #puts stderr ">>>jaws yielding value: $reduction triggered by word $word in position:$w"
-
-
- set _ID_ [yield $reduction]
- set stack [list]
- #set OID [::pattern::get_oid $_ID_]
- set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid
-
- if {$OID ne "null"} {
- set MAP [set ::p::${OID}::_meta::map] ;#Do not use upvar here!
- } else {
- set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] interfaces [list level0 {} level1 {}]]
- #puts stderr "WARNING REVIEW: jaws-branch - leave empty??????"
- }
-
- #review - 2018. switched to _ID_ instead of MAP
- lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command
- #lassign [dict get $MAP invocantdata] OID alias default_method object_command
-
-
- #puts stdout "---->>> yielded _ID_: $_ID_ OID:$OID alias:$alias default_method:$default_method object_command:$object_command"
- set operator $word
- #don't incr w
- #incr w
- } else {
- if {$operator eq "argprotect"} {
- set operator $operator_prev
- set operator_prev ""
- lappend stack $word
- } else {
- #only look for leading argprotect chacter (-) if we're not already in argprotect mode
- if {$word eq "--"} {
- set operator_prev $operator
- set operator "argprotect"
- #Don't add the plain argprotector to the stack
- } elseif {[string match "-*" $word]} {
- #argSafety operator (tokens that appear to be Tcl 'options' automatically 'protect' the subsequent argument)
- set operator_prev $operator
- set operator "argprotect"
- lappend stack $word
- } else {
- lappend stack $word
- }
- }
-
-
- incr w
- }
- } else {
- #no stack
- switch -- $word {.} {
-
- if {$OID ne "null"} {
- #we know next word is a property or method of a pattern object
- incr w
- set nextword [lindex $args [expr {$w - 1}]]
- set command ::p::${OID}::$nextword
- set stack [list $command] ;#2018 j
- set operator .
- if {$w eq $wordcount} {
- set finished_args 1
- }
- } else {
- # don't incr w
- #set nextword [lindex $args [expr {$w - 1}]]
- set command $object_command ;#taken from the MAP
- set stack [list "_exec_" $command]
- set operator .
- }
-
-
- } {..} {
- incr w
- set nextword [lindex $args [expr {$w -1}]]
- set command ::p::-1::$nextword
- #lappend stack $command ;#lappend a small number of items to an empty list is slower than just setting the list.
- set stack [list $command] ;#faster, and intent is clearer than lappend.
- set operator ..
- if {$w eq $wordcount} {
- set finished_args 1
- }
- } {,} {
- #puts stdout "Stackless comma!"
-
-
- if {$OID ne "null"} {
- set command ::p::${OID}::$default_method
- } else {
- set command [list $default_method $object_command]
- #object_command in this instance presumably be a list and $default_method a list operation
- #e.g "lindex {A B C}"
- }
- #lappend stack $command
- set stack [list $command]
- set operator ,
- } {--} {
- set operator_prev $operator
- set operator argprotect
- #no stack -
- } {!} {
- set command $object_command
- set stack [list "_exec_" $object_command]
- #puts stdout "!!!! !!!! $stack"
- set operator !
- } default {
- if {$operator eq ""} {
- if {$OID ne "null"} {
- set command ::p::${OID}::$default_method
- } else {
- set command [list $default_method $object_command]
- }
- set stack [list $command]
- set operator ,
- lappend stack $word
- } else {
- #no stack - so we don't expect to be in argprotect mode already.
- if {[string match "-*" $word]} {
- #argSafety operator (tokens that appear to be Tcl 'options' automatically 'protect' the subsequent argument)
- set operator_prev $operator
- set operator "argprotect"
- lappend stack $word
- } else {
- lappend stack $word
- }
-
- }
- }
- incr w
- }
-
- }
- } ;#end while
-
- #process final word outside of loop
- #assert $w == $wordcount
- #trailing operators or last argument
- if {!$finished_args} {
- set word [lindex $args [expr {$w -1}]]
- if {$operator eq "argprotect"} {
- set operator $operator_prev
- set operator_prev ""
-
- lappend stack $word
- incr w
- } else {
-
-
- switch -- $word {.} {
- if {![llength $stack]} {
- #set stack [list "_result_" [::p::internals::ref_to_object $_ID_]]
- yieldto return [::p::internals::ref_to_object $_ID_]
- error "assert: never gets here"
-
- } else {
- #puts stdout "==== $stack"
- #assert - whenever _ID_ changed in this proc - we have updated the $OID variable
- yieldto return [::p::internals::ref_to_stack $OID $_ID_ $stack]
- error "assert: never gets here"
- }
- set operator .
-
- } {..} {
- #trailing .. after chained call e.g >x . item 0 ..
- #puts stdout "$$$$$$$$$$$$ [list 0 $_ID_ {*}$stack] $$$$"
- #set reduction [list 0 $_ID_ {*}$stack]
- yieldto return [yield [list 0 $_ID_ {*}$stack]]
- } {#} {
- set unsupported 1
- } {,} {
- set unsupported 1
- } {&} {
- set unsupported 1
- } {@} {
- set unsupported 1
- } {--} {
-
- #set reduction [list 0 $_ID_ {*}$stack[set stack [list]]]
- #puts stdout " -> -> -> about to call yield $reduction <- <- <-"
- set _ID_ [yield [list 0 $_ID_ {*}$stack[set stack [list]]] ]
- #set OID [::pattern::get_oid $_ID_]
- set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid
-
- if {$OID ne "null"} {
- set MAP [set ::p::${OID}::_meta::map] ;#DO not use upvar here!
- } else {
- set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] interfaces {level0 {} level1 {}} ]
- }
- yieldto return $MAP
- } {!} {
- #error "untested branch"
- set _ID_ [yield [list 0 $_ID_ {*}$stack[set stack [list]]]]
- #set OID [::pattern::get_oid $_ID_]
- set OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid
-
- if {$OID ne "null"} {
- set MAP [set ::p::${OID}::_meta::map] ;#DO not use upvar here!
- } else {
- set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] ]
- }
- lassign [dict get $MAP invocantdata] OID alias default_command object_command
- set command $object_command
- set stack [list "_exec_" $command]
- set operator !
- } default {
- if {$operator eq ""} {
- #error "untested branch"
- lassign [dict get $MAP invocantdata] OID alias default_command object_command
- #set command ::p::${OID}::item
- set command ::p::${OID}::$default_command
- lappend stack $command
- set operator ,
-
- }
- #do not look for argprotect items here (e.g -option) as the final word can't be an argprotector anyway.
- lappend stack $word
- }
- if {$unsupported} {
- set unsupported 0
- error "trailing '$word' not supported"
-
- }
-
- #if {$operator eq ","} {
- # incr wordcount 2
- # set stack [linsert $stack end-1 . item]
- #}
- incr w
- }
- }
-
-
- #final = 1
- #puts stderr ">>>jaws final return value: [list 1 $_ID_ {*}$stack]"
-
- return [list 1 $_ID_ {*}$stack]
-}
-
-
-
-#trailing. directly after object
-proc ::p::internals::ref_to_object {_ID_} {
- set OID [lindex [dict get $_ID_ i this] 0 0]
- upvar #0 ::p::${OID}::_meta::map MAP
- lassign [dict get $MAP invocantdata] OID alias default_method object_command
- set refname ::p::${OID}::_ref::__OBJECT
-
- array set $refname [list] ;#important to initialise the variable as an array here - or initial read attempts on elements will not fire traces
-
- set traceCmd [list ::p::predator::object_read_trace $OID $_ID_]
- if {[list {read} $traceCmd] ni [trace info variable $refname]} {
- #puts stdout "adding read trace on variable '$refname' - traceCmd:'$traceCmd'"
- trace add variable $refname {read} $traceCmd
- }
- set traceCmd [list ::p::predator::object_array_trace $OID $_ID_]
- if {[list {array} $traceCmd] ni [trace info variable $refname]} {
- trace add variable $refname {array} $traceCmd
- }
-
- set traceCmd [list ::p::predator::object_write_trace $OID $_ID_]
- if {[list {write} $traceCmd] ni [trace info variable $refname]} {
- trace add variable $refname {write} $traceCmd
- }
-
- set traceCmd [list ::p::predator::object_unset_trace $OID $_ID_]
- if {[list {unset} $traceCmd] ni [trace info variable $refname]} {
- trace add variable $refname {unset} $traceCmd
- }
- return $refname
-}
-
-
-proc ::p::internals::create_or_update_reference {OID _ID_ refname command} {
- #if {[lindex $fullstack 0] eq "_exec_"} {
- # #strip it. This instruction isn't relevant for a reference.
- # set commandstack [lrange $fullstack 1 end]
- #} else {
- # set commandstack $fullstack
- #}
- #set argstack [lassign $commandstack command]
- #set field [string map {> __OBJECT_} [namespace tail $command]]
-
-
-
- set reftail [namespace tail $refname]
- set argstack [lassign [split $reftail +] field]
- set field [string map {> __OBJECT_} [namespace tail $command]]
-
- #puts stderr "refname:'$refname' command: $command field:$field"
-
-
- if {$OID ne "null"} {
- upvar #0 ::p::${OID}::_meta::map MAP
- } else {
- #set map [dict get [lindex [dict get $_ID_ i this] 0 1] map]
- set MAP [list invocantdata [lindex [dict get $_ID_ i this] 0] interfaces {level0 {} level1 {}}]
- }
- lassign [dict get $MAP invocantdata] OID alias default_method object_command
-
-
-
- if {$OID ne "null"} {
- interp alias {} $refname {} $command $_ID_ {*}$argstack
- } else {
- interp alias {} $refname {} $command {*}$argstack
- }
-
-
- #set iflist [lindex $map 1 0]
- set iflist [dict get $MAP interfaces level0]
- #set iflist [dict get $MAP interfaces level0]
- set field_is_property_like 0
- foreach IFID [lreverse $iflist] {
- #tcl (braced) expr has lazy evaluation for &&, || & ?: operators - so this should be reasonably efficient.
- if {[llength [info commands ::p::${IFID}::_iface::(GET)$field]] || [llength [info commands ::p::${IFID}::_iface::(SET)$field]]} {
- set field_is_property_like 1
- #There is a setter or getter (but not necessarily an entry in the o_properties dict)
- break
- }
- }
-
-
-
-
- #whether field is a property or a method - remove any commandrefMisuse_TraceHandler
- foreach tinfo [trace info variable $refname] {
- #puts "-->removing traces on $refname: $tinfo"
- if {[lindex $tinfo 1 0] eq "::p::internals::commandrefMisuse_TraceHandler"} {
- trace remove variable $refname {*}$tinfo
- }
- }
-
- if {$field_is_property_like} {
- #property reference
-
-
- set this_invocantdata [lindex [dict get $_ID_ i this] 0]
- lassign $this_invocantdata OID _alias _defaultmethod object_command
- #get fully qualified varspace
-
- #
- set propdict [$object_command .. GetPropertyInfo $field]
- if {[dict exist $propdict $field]} {
- set field_is_a_property 1
- set propinfo [dict get $propdict $field]
- set varspace [dict get $propinfo varspace]
- if {$varspace eq ""} {
- set full_varspace ::p::${OID}
- } else {
- if {[::string match "::*" $varspace]} {
- set full_varspace $varspace
- } else {
- set full_varspace ::p::${OID}::$varspace
- }
- }
- } else {
- set field_is_a_property 0
- #no propertyinfo - this field was probably established as a PropertyRead and/or PropertyWrite without a Property
- #this is ok - and we still set the trace infrastructure below (app may convert it to a normal Property later)
- set full_varspace ::p::${OID}
- }
-
-
-
-
-
- #We only trace on entire property.. not array elements (if references existed to both the array and an element both traces would be fired -(entire array trace first))
- set Hndlr [::list ::p::predator::propvar_write_TraceHandler $OID $field]
- if { [::list {write} $Hndlr] ni [trace info variable ${full_varspace}::o_${field}]} {
- trace add variable ${full_varspace}::o_${field} {write} $Hndlr
- }
- set Hndlr [::list ::p::predator::propvar_unset_TraceHandler $OID $field]
- if { [::list {unset} $Hndlr] ni [trace info variable ${full_varspace}::o_${field}]} {
- trace add variable ${full_varspace}::o_${field} {unset} $Hndlr
- }
-
-
- #supply all data in easy-access form so that propref_trace_read is not doing any extra work.
- set get_cmd ::p::${OID}::(GET)$field
- set traceCmd [list ::p::predator::propref_trace_read $get_cmd $_ID_ $refname $field $argstack]
-
- if {[list {read} $traceCmd] ni [trace info variable $refname]} {
- set fieldvarname ${full_varspace}::o_${field}
-
-
- #synch the refvar with the real var if it exists
- #catch {set $refname [$refname]}
- if {[array exists $fieldvarname]} {
- if {![llength $argstack]} {
- #unindexed reference
- array set $refname [array get $fieldvarname]
- #upvar $fieldvarname $refname
- } else {
- set s0 [lindex $argstack 0]
- #refs to nonexistant array members common? (catch vs 'info exists')
- if {[info exists ${fieldvarname}($s0)]} {
- set $refname [set ${fieldvarname}($s0)]
- }
- }
- } else {
- #refs to uninitialised props actually should be *very* common.
- #If we use 'catch', it means retrieving refs to non-initialised props is slower. Fired catches can be relatively expensive.
- #Because it's common to get a ref to uninitialised props (e.g for initial setting of their value) - we will use 'info exists' instead of catch.
-
- #set errorInfo_prev $::errorInfo ;#preserve errorInfo across catches!
-
- #puts stdout " ---->>!!! ref to uninitialised prop $field $argstack !!!<------"
-
-
- if {![llength $argstack]} {
- #catch {set $refname [set ::p::${OID}::o_$field]}
- if {[info exists $fieldvarname]} {
- set $refname [set $fieldvarname]
- #upvar $fieldvarname $refname
- }
- } else {
- if {[llength $argstack] == 1} {
- #catch {set $refname [lindex [set ::p::${OID}::o_$field] [lindex $argstack 0]]}
- if {[info exists $fieldvarname]} {
- set $refname [lindex [set $fieldvarname] [lindex $argstack 0]]
- }
-
- } else {
- #catch {set $refname [lindex [set ::p::${OID}::o_$field] $argstack]}
- if {[info exists $fieldvarname]} {
- set $refname [lindex [set $fieldvarname] $argstack]
- }
- }
- }
-
- #! what if someone has put a trace on ::errorInfo??
- #set ::errorInfo $errorInfo_prev
- }
- trace add variable $refname {read} $traceCmd
-
- set traceCmd [list ::p::predator::propref_trace_write $_ID_ $OID $full_varspace $refname]
- trace add variable $refname {write} $traceCmd
-
- set traceCmd [list ::p::predator::propref_trace_unset $_ID_ $OID $refname]
- trace add variable $refname {unset} $traceCmd
-
-
- set traceCmd [list ::p::predator::propref_trace_array $_ID_ $OID $refname]
- # puts "**************** installing array variable trace on ref:$refname - cmd:$traceCmd"
- trace add variable $refname {array} $traceCmd
- }
-
- } else {
- #puts "$refname ====> adding refMisuse_traceHandler $alias $field"
- #matching variable in order to detect attempted use as property and throw error
-
- #2018
- #Note that we are adding a trace on a variable (the refname) which does not exist.
- #this is fine - except that the trace won't fire for attempt to write it as an array using syntax such as set $ref(someindex)
- #we could set the ref to an empty array - but then we have to also undo this if a property with matching name is added
- ##array set $refname {} ;#empty array
- # - the empty array would mean a slightly better error message when misusing a command ref as an array
- #but this seems like a code complication for little benefit
- #review
-
- trace add variable $refname {read write unset array} [list ::p::internals::commandrefMisuse_TraceHandler $OID $field]
- }
-}
-
-
-
-#trailing. after command/property
-proc ::p::internals::ref_to_stack {OID _ID_ fullstack} {
- if {[lindex $fullstack 0] eq "_exec_"} {
- #strip it. This instruction isn't relevant for a reference.
- set commandstack [lrange $fullstack 1 end]
- } else {
- set commandstack $fullstack
- }
- set argstack [lassign $commandstack command]
- set field [string map {> __OBJECT_} [namespace tail $command]]
-
-
- #!todo?
- # - make every object's OID unpredictable and sparse (UUID) and modify 'namespace child' etc to prevent iteration/inspection of ::p namespace.
- # - this would only make sense for an environment where any meta methods taking a code body (e.g .. Method .. PatternMethod etc) are restricted.
-
-
- #references created under ::p::${OID}::_ref are effectively inside a 'varspace' within the object itself.
- # - this would in theory allow a set of interface functions on the object which have direct access to the reference variables.
-
-
- set refname ::p::${OID}::_ref::[join [concat $field $argstack] +]
-
- if {[llength [info commands $refname]]} {
- #todo - review - what if the field changed to/from a property/method?
- #probably should fix that where such a change is made and leave this short circuit here to give reasonable performance for existing refs
- return $refname
- }
- ::p::internals::create_or_update_reference $OID $_ID_ $refname $command
- return $refname
-}
-
-
-namespace eval pp {
- variable operators [list .. . -- - & @ # , !]
- variable operators_notin_args ""
- foreach op $operators {
- append operators_notin_args "({$op} ni \$args) && "
- }
- set operators_notin_args [string trimright $operators_notin_args " &"] ;#trim trailing spaces and ampersands
- #set operators_notin_args {({.} ni $args) && ({,} ni $args) && ({..} ni $args)}
-}
-interp alias {} strmap {} string map ;#stop code editor from mono-colouring our big string mapped code blocks!
-
-
-
-
-
-# 2017 ::p::predator2 is the development version - intended for eventual use as the main dispatch mechanism.
-#each map is a 2 element list of lists.
-# form: {$commandinfo $interfaceinfo}
-# commandinfo is of the form: {ID Namespace defaultmethod commandname _?}
-
-#2018
-#each map is a dict.
-#form: {invocantdata {ID Namespace defaultmethod commandname _?} interfaces {level0 {} level1 {}}}
-
-
-#OID = Object ID (integer for now - could in future be a uuid)
-proc ::p::predator2 {_ID_ args} {
- #puts stderr "predator2: _ID_:'$_ID_' args:'$args'"
- #set invocants [dict get $_ID_ i]
- #set invocant_roles [dict keys $invocants]
-
- #For now - we are 'this'-centric (single dispatch). todo - adapt for multiple roles, multimethods etc.
- #set this_role_members [dict get $invocants this]
- #set this_invocant [lindex [dict get $_ID_ i this] 0] ;#for the role 'this' we assume only one invocant in the list.
- #lassign $this_invocant this_OID this_info_dict
-
- set this_OID [lindex [dict get $_ID_ i this] 0 0] ;#get_oid
-
-
- set cheat 1 ;#
- #-------
- #Optimise the next most common use case. A single . followed by args which contain no other operators (non-chained call)
- #(it should be functionally equivalent to remove this shortcut block)
- if {$cheat} {
- if { ([lindex $args 0] eq {.}) && ([llength $args] > 1) && ([llength [lsearch -all -inline $args .]] == 1) && ({,} ni $args) && ({..} ni $args) && ({--} ni $args) && ({!} ni $args)} {
-
- set remaining_args [lassign $args dot method_or_prop]
-
- #how will we do multiple apis? (separate interface stacks) apply? apply [list [list _ID_ {*}$arglist] ::p::${stackid?}::$method_or_prop ::p::${this_OID}] ???
- set command ::p::${this_OID}::$method_or_prop
- #REVIEW!
- #e.g what if the method is named "say hello" ?? (hint - it will break because we will look for 'say')
- #if {[llength $command] > 1} {
- # error "methods with spaces not included in test suites - todo fix!"
- #}
- #Dont use {*}$command - (so we can support methods with spaces)
- #if {![llength [info commands $command]]} {}
- if {[namespace which $command] eq ""} {
- if {[namespace which ::p::${this_OID}::(UNKNOWN)] ne ""} {
- #lset command 0 ::p::${this_OID}::(UNKNOWN) ;#seems wrong - command could have spaces
- set command ::p::${this_OID}::(UNKNOWN)
- #tailcall {*}$command $_ID_ $cmdname {*}[lrange $args 2 end] ;#delegate to UNKNOWN, along with original commandname as 1st arg.
- tailcall $command $_ID_ $method_or_prop {*}[lrange $args 2 end] ;#delegate to UNKNOWN, along with original commandname as 1st arg.
- } else {
- return -code error -errorinfo "(::p::predator2) error running command:'$command' argstack:'[lrange $args 2 end]'\n - command not found and no 'unknown' handler" "method '$method_or_prop' not found"
- }
- } else {
- #tailcall {*}$command $_ID_ {*}$remaining_args
- tailcall $command $_ID_ {*}$remaining_args
- }
- }
- }
- #------------
-
-
- if {([llength $args] == 1) && ([lindex $args 0] eq "..")} {
- return $_ID_
- }
-
-
- #puts stderr "pattern::predator (test version) called with: _ID_:$_ID_ args:$args"
-
-
-
- #puts stderr "this_info_dict: $this_info_dict"
-
-
-
-
- if {![llength $args]} {
- #should return some sort of public info.. i.e probably not the ID which is an implementation detail
- #return cmd
- return [lindex [dict get [set ::p::${this_OID}::_meta::map] invocantdata] 0] ;#Object ID
-
- #return a dict keyed on object command name - (suitable as use for a .. Create 'target')
- #lassign [dict get [set ::p::${this_OID}::_meta::map] invocantdata] this_OID alias default_method object_command wrapped
- #return [list $object_command [list -id $this_OID ]]
- } elseif {[llength $args] == 1} {
- #short-circuit the single index case for speed.
- if {[lindex $args 0] ni {.. . -- - & @ # , !}} {
- #lassign [dict get [set ::p::${this_OID}::_meta::map] invocantdata] this_OID alias default_method
- lassign [lindex [dict get $_ID_ i this] 0] this_OID alias default_method
-
- tailcall ::p::${this_OID}::$default_method $_ID_ [lindex $args 0]
- } elseif {[lindex $args 0] eq {--}} {
-
- #!todo - we could hide the invocant by only allowing this call from certain uplevel procs..
- # - combined with using UUIDs for $OID, and a secured/removed metaface on the object
- # - (and also hiding of [interp aliases] command so they can't iterate and examine all aliases)
- # - this could effectively hide the object's namespaces,vars etc from the caller (?)
- return [set ::p::${this_OID}::_meta::map]
- }
- }
-
-
-
- #upvar ::p::coroutine_instance c ;#coroutine names must be unique per call to predator (not just per object - or we could get a clash during some cyclic calls)
- #incr c
- #set reduce ::p::reducer${this_OID}_$c
- set reduce ::p::reducer${this_OID}_[incr ::p::coroutine_instance]
- #puts stderr "..................creating reducer $reduce with args $this_OID _ID_ $args"
- coroutine $reduce ::p::internals::jaws $this_OID $_ID_ {*}$args
-
-
- set current_ID_ $_ID_
-
- set final 0
- set result ""
- while {$final == 0} {
- #the argument given here to $reduce will be returned by 'yield' within the coroutine context (jaws)
- set reduction_args [lassign [$reduce $current_ID_[set current_ID_ [list]] ] final current_ID_ command]
- #puts stderr "..> final:$final current_ID_:'$current_ID_' command:'$command' reduction_args:'$reduction_args'"
- #if {[string match *Destroy $command]} {
- # puts stdout " calling Destroy reduction_args:'$reduction_args'"
- #}
- if {$final == 1} {
-
- if {[llength $command] == 1} {
- if {$command eq "_exec_"} {
- tailcall {*}$reduction_args
- }
- if {[llength [info commands $command]]} {
- tailcall {*}$command $current_ID_ {*}$reduction_args
- }
- set cmdname [namespace tail $command]
- set this_OID [lindex [dict get $current_ID_ i this] 0 0]
- if {[llength [info commands ::p::${this_OID}::(UNKNOWN)]]} {
- lset command 0 ::p::${this_OID}::(UNKNOWN)
- tailcall {*}$command $current_ID_ $cmdname {*}$reduction_args ;#delegate to UNKNOWN, along with original commandname as 1st arg.
- } else {
- return -code error -errorinfo "1)error running command:'$command' argstack:'$reduction_args'\n - command not found and no 'unknown' handler" "method '$cmdname' not found"
- }
-
- } else {
- #e.g lindex {a b c}
- tailcall {*}$command {*}$reduction_args
- }
-
-
- } else {
- if {[lindex $command 0] eq "_exec_"} {
- set result [uplevel 1 [list {*}[lrange $command 1 end] {*}$reduction_args]]
-
- set current_ID_ [list i [list this [list [list "null" {} {lindex} $result {} ] ] ] context {} ]
- } else {
- if {[llength $command] == 1} {
- if {![llength [info commands $command]]} {
- set cmdname [namespace tail $command]
- set this_OID [lindex [dict get $current_ID_ i this] 0 0]
- if {[llength [info commands ::p::${this_OID}::(UNKNOWN)]]} {
-
- lset command 0 ::p::${this_OID}::(UNKNOWN)
- set result [uplevel 1 [list {*}$command $current_ID_ $cmdname {*}$reduction_args]] ;#delegate to UNKNOWN, along with original commandname as 1st arg.
- } else {
- return -code error -errorinfo "2)error running command:'$command' argstack:'$reduction_args'\n - command not found and no 'unknown' handler" "method '$cmdname' not found"
- }
- } else {
- #set result [uplevel 1 [list {*}$command $current_ID_ {*}$reduction_args ]]
- set result [uplevel 1 [list {*}$command $current_ID_ {*}$reduction_args ]]
-
- }
- } else {
- set result [uplevel 1 [list {*}$command {*}$reduction_args]]
- }
-
- if {[llength [info commands $result]]} {
- if {([llength $result] == 1) && ([string first ">" [namespace tail $result]] == 0)} {
- #looks like a pattern command
- set current_ID_ [$result .. INVOCANTDATA]
-
-
- #todo - determine if plain .. INVOCANTDATA is sufficient instead of .. UPDATEDINVOCANTDATA
- #if {![catch {$result .. INVOCANTDATA} result_invocantdata]} {
- # set current_ID_ $result_invocantdata
- #} else {
- # return -code error -errorinfo "3)error running command:'$command' argstack:'$reduction_args'\n - Failed to access result:'$result' as a pattern object." "Failed to access result:'$result' as a pattern object"
- #}
- } else {
- #non-pattern command
- set current_ID_ [list i [list this [list [list "null" {} {lindex} $result {} ] ] ] context {}]
- }
- } else {
- set current_ID_ [list i [list this [list [list "null" {} {lindex} $result {} ] ] ] context {}]
- #!todo - allow further operations on non-command values. e.g dicts, lists & strings (treat strings as lists)
-
- }
- }
-
- }
- }
- error "Assert: Shouldn't get here (end of ::p::predator2)"
- #return $result
-}
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/promise-1.2.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/promise-1.2.0.tm
deleted file mode 100644
index a4b82e45..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/promise-1.2.0.tm
+++ /dev/null
@@ -1,1311 +0,0 @@
-# Copyright (c) 2015-2023, Ashok P. Nadkarni
-# All rights reserved.
-
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-package require Tcl 8.6-
-
-namespace eval promise {
- proc version {} { return 1.2.0 }
-}
-
-proc promise::lambda {params body args} {
- # Creates an anonymous procedure and returns a command prefix for it.
- # params - parameter definitions for the procedure
- # body - body of the procedures
- # args - additional arguments to be passed to the procedure when it
- # is invoked
- #
- # This is just a convenience command since anonymous procedures are
- # commonly useful with promises. The lambda package from tcllib
- # is identical in function.
-
- return [list ::apply [list $params $body] {*}$args]
-}
-
-catch {promise::Promise destroy}
-oo::class create promise::Promise {
-
- # The promise state can be one of
- # PENDING - Initial state where it has not yet been assigned a
- # value or error
- # FULFILLED - The promise has been assigned a value
- # REJECTED - The promise has been assigned an error
- # CHAINED - The promise is attached to another promise
- variable _state
-
- # Stores data that is accessed through the setdata/getdata methods.
- # The Promise class itself does not use this.
- variable _clientdata
-
- # The promise value once it is fulfilled or rejected. In the latter
- # case, it should be an the error message
- variable _value
-
- # The error dictionary in case promise is rejected
- variable _edict
-
- # Reactions to be notified when the promise is rejected. Each element
- # in this list is a pair consisting of the fulfilment reaction
- # and the rejection reaction. Either element of the pair could be
- # empty signifying no reaction for that case. The list is populated
- # via the then method.
- variable _reactions
-
- # Reference counting to free up promises since Tcl does not have
- # garbage collection for objects. Garbage collection via reference
- # counting only takes place after at least one done/then reaction
- # is placed on the event queue, not before. Else promises that
- # are immediately resolved on construction would be freed right
- # away before the application even gets a chance to call done/then.
- variable _do_gc
- variable _nrefs
-
- # If no reject reactions are registered, then the Tcl bgerror
- # handler is invoked. But don't want to do this more than once
- # so track it
- variable _bgerror_done
-
- constructor {cmd} {
- # Create a promise for the asynchronous operation to be initiated
- # by $cmd.
- # cmd - a command prefix that should initiate an asynchronous
- # operation.
- # The command prefix $cmd is passed an additional argument - the
- # name of this Promise object. It should arrange for one of the
- # object's settle methods [fulfill], [chain] or
- # [reject] to be called when the operation completes.
-
- set _state PENDING
- set _reactions [list ]
- set _do_gc 0
- set _bgerror_done 0
- set _nrefs 0
- array set _clientdata {}
-
- # Errors in the construction command are returned via
- # the standard mechanism of reject.
- #
- if {[catch {
- # For some special cases, $cmd may be "" if the async operation
- # is initiated outside the constructor. This is not a good
- # thing because the error in the initiator will not be
- # trapped via the standard promise error catching mechanism
- # but that's the application's problem (actually pgeturl also
- # uses this).
- if {[llength $cmd]} {
- uplevel #0 [linsert $cmd end [self]]
- }
- } msg edict]} {
- my reject $msg $edict
- }
- }
-
- destructor {
- # Destroys the object.
- #
- # This method should not be generally called directly as [Promise]
- # objects are garbage collected either automatically or via the [ref]
- # and [unref] methods.
- }
-
- method state {} {
- # Returns the current state of the promise.
- #
- # The promise state may be one of the values `PENDING`,
- # `FULFILLED`, `REJECTED` or `CHAINED`
- return $_state
- }
-
- method getdata {key} {
- # Returns data previously stored through the setdata method.
- # key - key whose associated values is to be returned.
- # An error will be raised if no value is associated with the key.
- return $_clientdata($key)
- }
-
- method setdata {key value} {
- # Sets a value to be associated with a key.
- # key - the lookup key
- # value - the value to be associated with the key
- # A promise internally maintains a dictionary whose values can
- # be accessed with the [getdata] and [setdata] methods. This
- # dictionary is not used by the Promise class itself but is meant
- # to be used by promise library specializations or applications.
- # Callers need to take care that keys used for a particular
- # promise are sufficiently distinguishable so as to not clash.
- #
- # Returns the value stored with the key.
- set _clientdata($key) $value
- }
-
- method value {} {
- # Returns the settled value for the promise.
- #
- # The returned value may be the fulfilled value or the rejected
- # value depending on whether the associated operation was successfully
- # completed or failed.
- #
- # An error is raised if the promise is not settled yet.
- if {$_state ni {FULFILLED REJECTED}} {
- error "Value is not set."
- }
- return $_value
- }
-
- method ref {} {
- # Increments the reference count for the object.
- incr _nrefs
- }
-
- method unref {} {
- # Decrements the reference count for the object.
- #
- # The object may have been destroyed when the call returns.
- incr _nrefs -1
- my GC
- }
-
- method nrefs {} {
- # Returns the current reference count.
- #
- # Use for debugging only! Note, internal references are not included.
- return $_nrefs
- }
-
- method GC {} {
- if {$_nrefs <= 0 && $_do_gc && [llength $_reactions] == 0} {
- my destroy
- }
- }
-
- method FulfillAttached {value} {
- if {$_state ne "CHAINED"} {
- return
- }
- set _value $value
- set _state FULFILLED
- my ScheduleReactions
- return
- }
-
- method RejectAttached {reason edict} {
- if {$_state ne "CHAINED"} {
- return
- }
- set _value $reason
- set _edict $edict
- set _state REJECTED
- my ScheduleReactions
- return
- }
-
- # Method to invoke to fulfil a promise with a value or another promise.
- method fulfill {value} {
- # Fulfills the promise.
- # value - the value with which the promise is fulfilled
- #
- # Returns `0` if promise had already been settled and `1` if
- # it was fulfilled by the current call.
-
- #ruff
- # If the promise has already been settled, the method has no effect.
- if {$_state ne "PENDING"} {
- return 0; # Already settled
- }
-
- #ruff
- # Otherwise, it is transitioned to the `FULFILLED` state with
- # the value specified by $value. If there are any fulfillment
- # reactions registered by the [Promise.done] or [Promise.then] methods, they
- # are scheduled to be run.
- set _value $value
- set _state FULFILLED
- my ScheduleReactions
- return 1
- }
-
- # Method to invoke to fulfil a promise with a value or another promise.
- method chain {promise} {
- # Chains the promise to another promise.
- # promise - the [Promise] object to which this promise is to
- # be chained
- #
- # Returns `0` if promise had already been settled and `1` otherwise.
-
- #ruff
- # If the promise on which this method is called
- # has already been settled, the method has no effect.
- if {$_state ne "PENDING"} {
- return 0;
- }
-
- #ruff
- # Otherwise, it is chained to $promise so that it reflects that
- # other promise's state.
- if {[catch {
- $promise done [namespace code {my FulfillAttached}] [namespace code {my RejectAttached}]
- } msg edict]} {
- my reject $msg $edict
- } else {
- set _state CHAINED
- }
-
- return 1
- }
-
- method reject {reason {edict {}}} {
- # Rejects the promise.
- # reason - a message string describing the reason for the rejection.
- # edict - a Tcl error dictionary
- #
- # The $reason and $edict values are passed on to the rejection
- # reactions. By convention, these should be of the form returned
- # by the `catch` or `try` commands in case of errors.
- #
- # Returns `0` if promise had already been settled and `1` if
- # it was rejected by the current call.
-
- #ruff
- # If the promise has already been settled, the method has no effect.
- if {$_state ne "PENDING"} {
- return 0; # Already settled
- }
-
- #ruff
- # Otherwise, it is transitioned to the `REJECTED` state. If
- # there are any reject reactions registered by the [Promise.done] or
- # [Promise.then] methods, they are scheduled to be run.
-
- set _value $reason
- #ruff
- # If $edict is not specified, or specified as an empty string,
- # a suitable error dictionary is constructed in its place
- # to be passed to the reaction.
- if {$edict eq ""} {
- catch {throw {PROMISE REJECTED} $reason} - edict
- }
- set _edict $edict
- set _state REJECTED
- my ScheduleReactions
- return 1
- }
-
- # Internal method to queue all registered reactions based on
- # whether the promise is succesfully fulfilled or not
- method ScheduleReactions {} {
- if {$_state ni {FULFILLED REJECTED} || [llength $_reactions] == 0 } {
- # Promise is not settled or no reactions registered
- return
- }
-
- # Note on garbage collection: garbage collection is to be enabled if
- # at least one FULFILLED or REJECTED reaction is registered.
- # Also if the promise is REJECTED but no rejection handlers are run
- # we also schedule a background error.
- # In all cases, CLEANUP reactions do not count.
- foreach reaction $_reactions {
- foreach type {FULFILLED REJECTED} {
- if {[dict exists $reaction $type]} {
- set _do_gc 1
- if {$type eq $_state} {
- set cmd [dict get $reaction $type]
- if {[llength $cmd]} {
- if {$type eq "FULFILLED"} {
- lappend cmd $_value
- } else {
- lappend cmd $_value $_edict
- }
- set ran_reaction($type) 1
- # Enqueue the reaction via the event loop
- after 0 [list after idle $cmd]
- }
- }
- }
- }
- if {[dict exists $reaction CLEANUP]} {
- set cmd [dict get $reaction CLEANUP]
- if {[llength $cmd]} {
- # Enqueue the cleaner via the event loop passing the
- # *state* as well as the value
- if {$_state eq "REJECTED"} {
- lappend cmd $_state $_value $_edict
- } else {
- lappend cmd $_state $_value
- }
- after 0 [list after idle $cmd]
- # Note we do not set _do_gc if we only run cleaners
- }
- }
- }
- set _reactions [list ]
-
- # Check for need to background error (see comments above)
- if {$_state eq "REJECTED" && $_do_gc && ! [info exists ran_reaction(REJECTED)] && ! $_bgerror_done} {
- # TBD - should we also check _nrefs before backgrounding error?
-
- # Wrap in catch in case $_edict does not follow error conventions
- # or is not even a dictionary
- if {[catch {
- dict get $_edict -level
- dict get $_edict -code
- }]} {
- catch {throw {PROMISE REJECT} $_value} - edict
- } else {
- set edict $_edict
- }
- # TBD - how exactly is level to be handled?
- # If -level is not 0, bgerror barfs because it treates
- # it as TCL_RETURN no matter was -code is
- dict set edict -level 0
- after idle [interp bgerror {}] [list $_value $edict]
- set _bgerror_done 1
- }
-
- my GC
- return
- }
-
- method RegisterReactions {args} {
- # Registers the specified reactions.
- # args - dictionary keyed by `CLEANUP`, `FULFILLED`, `REJECTED`
- # with values being the corresponding reaction callback
-
- lappend _reactions $args
- my ScheduleReactions
- return
- }
-
- method done {{on_fulfill {}} {on_reject {}}} {
- # Registers reactions to be run when the promise is settled.
- # on_fulfill - command prefix for the reaction to run
- # if the promise is fulfilled.
- # reaction is registered.
- # on_reject - command prefix for the reaction to run
- # if the promise is rejected.
- # Reactions are called with an additional argument which is
- # the value with which the promise was settled.
- #
- # The command may be called multiple times to register multiple
- # reactions to be run at promise settlement. If the promise was
- # already settled at the time the call was made, the reactions
- # are invoked immediately. In all cases, reactions are not called
- # directly, but are invoked by scheduling through the event loop.
- #
- # The method triggers garbage collection of the object if the
- # promise has been settled and any registered reactions have been
- # scheduled. Applications can hold on to the object through
- # appropriate use of the [ref] and [unref] methods.
- #
- # Note that both $on_fulfill and $on_reject may be specified
- # as empty strings if no further action needs to be taken on
- # settlement of the promise. If the promise is rejected, and
- # no rejection reactions are registered, the error is reported
- # via the Tcl `interp bgerror` facility.
-
- # TBD - as per the Promise/A+ spec, errors in done should generate
- # a background error (unlike then).
-
- my RegisterReactions FULFILLED $on_fulfill REJECTED $on_reject
-
- #ruff
- # The method does not return a value.
- return
- }
-
- method then {on_fulfill {on_reject {}}} {
- # Registers reactions to be run when the promise is settled
- # and returns a new [Promise] object that will be settled by the
- # reactions.
- # on_fulfill - command prefix for the reaction to run
- # if the promise is fulfilled. If an empty string, no fulfill
- # reaction is registered.
- # on_reject - command prefix for the reaction to run
- # if the promise is rejected. If unspecified or an empty string,
- # no reject reaction is registered.
- # Both reactions are passed the value with which the promise was settled.
- # The reject reaction is passed an additional argument which is
- # the error dictionary.
- #
- # The command may be called multiple times to register multiple
- # reactions to be run at promise settlement. If the promise was
- # already settled at the time the call was made, the reactions
- # are invoked immediately. In all cases, reactions are not called
- # directly, but are invoked by scheduling through the event loop.
- #
- # If the reaction that is invoked runs without error, its return
- # value fulfills the new promise returned by the `then` method.
- # If it raises an exception, the new promise will be rejected
- # with the error message and dictionary from the exception.
- #
- # Alternatively, the reactions can explicitly invoke commands
- # [then_fulfill], [then_reject] or [then_chain] to
- # resolve the returned promise. In this case, the return value
- # (including exceptions) from the reactions are ignored.
- #
- # If `on_fulfill` (or `on_reject`) is an empty string (or unspecified),
- # the new promise is created and fulfilled (or rejected) with
- # the same value that would have been passed in to the reactions.
- #
- # The method triggers garbage collection of the object if the
- # promise has been settled and registered reactions have been
- # scheduled. Applications can hold on to the object through
- # appropriate use of the [ref] and [unref] methods.
- #
- # Returns a new promise that is settled by the registered reactions.
-
- set then_promise [[self class] new ""]
- my RegisterReactions \
- FULFILLED [list ::promise::_then_reaction $then_promise FULFILLED $on_fulfill] \
- REJECTED [list ::promise::_then_reaction $then_promise REJECTED $on_reject]
- return $then_promise
- }
-
- # This could be a forward, but then we cannot document it via ruff!
- method catch {on_reject} {
- # Registers reactions to be run when the promise is rejected.
- # on_reject - command prefix for the reaction
- # reaction to run if the promise is rejected. If unspecified
- # or an empty string, no reject reaction is registered. The
- # reaction is called with an additional argument which is the
- # value with which the promise was settled.
- # This method is just a wrapper around [Promise.then] with the
- # `on_fulfill` parameter defaulting to an empty string. See
- # the description of that method for details.
- return [my then "" $on_reject]
- }
-
- method cleanup {cleaner} {
- # Registers a reaction to be executed for running cleanup
- # code when the promise is settled.
- # cleaner - command prefix to run on settlement
- # This method is intended to run a clean up script
- # when a promise is settled. Its primary use is to avoid duplication
- # of code in the `then` and `catch` handlers for a promise.
- # It may also be called multiple times
- # to clean up intermediate steps when promises are chained.
- #
- # The method returns a new promise that will be settled
- # as per the following rules.
- # - if the cleaner runs without errors, the returned promise
- # will reflect the settlement of the promise on which this
- # method is called.
- # - if the cleaner raises an exception, the returned promise
- # is rejected with a value consisting of the error message
- # and dictionary pair.
- #
- # Returns a new promise that is settled based on the cleaner
- set cleaner_promise [[self class] new ""]
- my RegisterReactions CLEANUP [list ::promise::_cleanup_reaction $cleaner_promise $cleaner]
- return $cleaner_promise
- }
-}
-
-proc promise::_then_reaction {target_promise status cmd value {edict {}}} {
- # Run the specified command and fulfill/reject the target promise
- # accordingly. If the command is empty, the passed-in value is passed
- # on to the target promise.
-
- # IMPORTANT!!!!
- # MUST BE CALLED FROM EVENT LOOP AT so info level must be 1. Else
- # promise::then_fulfill/then_reject/then_chain will not work
- # Also, Do NOT change the param name target_promise without changing
- # those procs.
- # Oh what a hack to get around lack of closures. Alternative would have
- # been to pass an additional parameter (target_promise)
- # to the application code but then that script would have had to
- # carry that around.
-
- if {[info level] != 1} {
- error "Internal error: _then_reaction not at level 1"
- }
-
- if {[llength $cmd] == 0} {
- switch -exact -- $status {
- FULFILLED { $target_promise fulfill $value }
- REJECTED { $target_promise reject $value $edict}
- CHAINED -
- PENDING -
- default {
- $target_promise reject "Internal error: invalid status $state"
- }
- }
- } else {
- # Invoke the real reaction code and fulfill/reject the target promise.
- # Note the reaction code may have called one of the promise::then_*
- # commands itself and reactions run resulting in the object being
- # freed. Hence resolve using the safe* variants
- # TBD - ideally we would like to execute at global level. However
- # the then_* commands retrieve target_promise from level 1 (here)
- # which they cannot if uplevel #0 is done. So directly invoke.
- if {$status eq "REJECTED"} {
- lappend cmd $value $edict
- } else {
- lappend cmd $value
- }
- if {[catch $cmd reaction_value reaction_edict]} {
- safe_reject $target_promise $reaction_value $reaction_edict
- } else {
- safe_fulfill $target_promise $reaction_value
- }
- }
- return
-}
-
-proc promise::_cleanup_reaction {target_promise cleaner state value {edict {}}} {
- # Run the specified cleaner and fulfill/reject the target promise
- # accordingly. If the cleaner executes without error, the original
- # value and state is passed on. If the cleaner executes with error
- # the promise is rejected.
-
- if {[llength $cleaner] == 0} {
- switch -exact -- $state {
- FULFILLED { $target_promise fulfill $value }
- REJECTED { $target_promise reject $value $edict }
- CHAINED -
- PENDING -
- default {
- $target_promise reject "Internal error: invalid state $state"
- }
- }
- } else {
- if {[catch {uplevel #0 $cleaner} err edict]} {
- # Cleaner failed. Reject the target promise
- $target_promise reject $err $edict
- } else {
- # Cleaner completed without errors, pass on the original value
- if {$state eq "FULFILLED"} {
- $target_promise fulfill $value
- } else {
- $target_promise reject $value $edict
- }
- }
- }
- return
-}
-
-proc promise::then_fulfill {value} {
- # Fulfills the promise returned by a [Promise.then] method call from
- # within its reaction.
- # value - the value with which to fulfill the promise
- #
- # The [Promise.then] method is a mechanism to chain asynchronous
- # reactions by registering them on a promise. It returns a new
- # promise which is settled by the return value from the reaction,
- # or by the reaction calling one of three commands - `then_fulfill`,
- # [then_reject] or [then_chain]. Calling `then_fulfill` fulfills
- # the promise returned by the `then` method that queued the currently
- # running reaction.
- #
- # It is an error to call this command from outside a reaction
- # that was queued via the [Promise.then] method on a promise.
-
- # TBD - what if someone calls this from within a uplevel #0 ? The
- # upvar will be all wrong
- upvar #1 target_promise target_promise
- if {![info exists target_promise]} {
- set msg "promise::then_fulfill called in invalid context."
- throw [list PROMISE THEN FULFILL NOTARGET $msg] $msg
- }
- $target_promise fulfill $value
-}
-
-proc promise::then_chain {promise} {
- # Chains the promise returned by a [Promise.then] method call to
- # another promise.
- # promise - the promise to which the promise returned by [Promise.then] is
- # to be chained
- #
- # The [Promise.then] method is a mechanism to chain asynchronous
- # reactions by registering them on a promise. It returns a new
- # promise which is settled by the return value from the reaction,
- # or by the reaction calling one of three commands - [then_fulfill],
- # `then_reject` or [then_chain]. Calling `then_chain` chains
- # the promise returned by the `then` method that queued the currently
- # running reaction to $promise so that the former will be settled
- # based on the latter.
- #
- # It is an error to call this command from outside a reaction
- # that was queued via the [Promise.then] method on a promise.
- upvar #1 target_promise target_promise
- if {![info exists target_promise]} {
- set msg "promise::then_chain called in invalid context."
- throw [list PROMISE THEN FULFILL NOTARGET $msg] $msg
- }
- $target_promise chain $promise
-}
-
-proc promise::then_reject {reason edict} {
- # Rejects the promise returned by a [Promise.then] method call from
- # within its reaction.
- # reason - a message string describing the reason for the rejection.
- # edict - a Tcl error dictionary
- # The [Promise.then] method is a mechanism to chain asynchronous
- # reactions by registering them on a promise. It returns a new
- # promise which is settled by the return value from the reaction,
- # or by the reaction calling one of three commands - [then_fulfill],
- # `then_reject` or [then_chain]. Calling `then_reject` rejects
- # the promise returned by the `then` method that queued the currently
- # running reaction.
- #
- # It is an error to call this command from outside a reaction
- # that was queued via the [Promise.then] method on a promise.
- upvar #1 target_promise target_promise
- if {![info exists target_promise]} {
- set msg "promise::then_reject called in invalid context."
- throw [list PROMISE THEN FULFILL NOTARGET $msg] $msg
- }
- $target_promise reject $reason $edict
-}
-
-proc promise::all {promises} {
- # Returns a promise that fulfills or rejects when all promises
- # in the $promises argument have fulfilled or any one has rejected.
- # promises - a list of Promise objects
- # If any of $promises rejects, then the promise returned by the
- # command will reject with the same value. Otherwise, the promise
- # will fulfill when all promises have fulfilled.
- # The resolved value will be a list of the resolved
- # values of the contained promises.
-
- set all_promise [Promise new [lambda {promises prom} {
- set npromises [llength $promises]
- if {$npromises == 0} {
- $prom fulfill {}
- return
- }
-
- # Ask each promise to update us when resolved.
- foreach promise $promises {
- $promise done \
- [list ::promise::_all_helper $prom $promise FULFILLED] \
- [list ::promise::_all_helper $prom $promise REJECTED]
- }
-
- # We keep track of state with a dictionary that will be
- # stored in $prom with the following keys:
- # PROMISES - the list of promises in the order passed
- # PENDING_COUNT - count of unresolved promises
- # RESULTS - dictionary keyed by promise and containing resolved value
- set all_state [list PROMISES $promises PENDING_COUNT $npromises RESULTS {}]
-
- $prom setdata ALLPROMISES $all_state
- } $promises]]
-
- return $all_promise
-}
-
-proc promise::all* args {
- # Returns a promise that fulfills or rejects when all promises
- # in the $args argument have fulfilled or any one has rejected.
- # args - list of Promise objects
- # This command is identical to the all command except that it takes
- # multiple arguments, each of which is a Promise object. See [all]
- # for a description.
- return [all $args]
-}
-
-# Callback for promise::all.
-# all_promise - the "master" promise returned by the all call.
-# done_promise - the promise whose callback is being serviced.
-# resolution - whether the current promise was resolved with "FULFILLED"
-# or "REJECTED"
-# value - the value of the currently fulfilled promise or error description
-# in case rejected
-# edict - error dictionary (if promise was rejected)
-proc promise::_all_helper {all_promise done_promise resolution value {edict {}}} {
- if {![info object isa object $all_promise]} {
- # The object has been deleted. Naught to do
- return
- }
- if {[$all_promise state] ne "PENDING"} {
- # Already settled. This can happen when a tracked promise is
- # rejected and another tracked promise gets settled afterwards.
- return
- }
- if {$resolution eq "REJECTED"} {
- # This promise failed. Immediately reject the master promise
- # TBD - can we somehow indicate which promise failed ?
- $all_promise reject $value $edict
- return
- }
-
- # Update the state of the resolved tracked promise
- set all_state [$all_promise getdata ALLPROMISES]
- dict set all_state RESULTS $done_promise $value
- dict incr all_state PENDING_COUNT -1
- $all_promise setdata ALLPROMISES $all_state
-
- # If all promises resolved, resolve the all promise
- if {[dict get $all_state PENDING_COUNT] == 0} {
- set values {}
- foreach prom [dict get $all_state PROMISES] {
- lappend values [dict get $all_state RESULTS $prom]
- }
- $all_promise fulfill $values
- }
- return
-}
-
-proc promise::race {promises} {
- # Returns a promise that fulfills or rejects when any promise
- # in the $promises argument is fulfilled or rejected.
- # promises - a list of Promise objects
- # The returned promise will fulfill and reject with the same value
- # as the first promise in $promises that fulfills or rejects.
- set race_promise [Promise new [lambda {promises prom} {
- if {[llength $promises] == 0} {
- catch {throw {PROMISE RACE EMPTYSET} "No promises specified."} reason edict
- $prom reject $reason $edict
- return
- }
- # Use safe_*, do not directly call methods since $prom may be
- # gc'ed once settled
- foreach promise $promises {
- $promise done [list ::promise::safe_fulfill $prom ] [list ::promise::safe_reject $prom]
- }
- } $promises]]
-
- return $race_promise
-}
-
-proc promise::race* {args} {
- # Returns a promise that fulfills or rejects when any promise
- # in the passed arguments is fulfilled or rejected.
- # args - list of Promise objects
- # This command is identical to the `race` command except that it takes
- # multiple arguments, each of which is a Promise object. See [race]
- # for a description.
- return [race $args]
-}
-
-proc promise::await {prom} {
- # Waits for a promise to be settled and returns its resolved value.
- # prom - the promise that is to be waited on
- # This command may only be used from within a procedure constructed
- # with the [async] command or any code invoked from it.
- #
- # Returns the resolved value of $prom if it is fulfilled or raises an error
- # if it is rejected.
- set coro [info coroutine]
- if {$coro eq ""} {
- throw {PROMISE AWAIT NOTCORO} "await called from outside a coroutine"
- }
- $prom done [list $coro success] [list $coro fail]
- lassign [yieldto return -level 0] status val ropts
- if {$status eq "success"} {
- return $val
- } else {
- return -options $ropts $val
- }
-}
-
-proc promise::async {name paramdefs body} {
- # Defines an procedure that will run a script asynchronously as a coroutine.
- # name - name of the procedure
- # paramdefs - the parameter definitions to the procedure in the same
- # form as passed to the standard `proc` command
- # body - the script to be executed
- #
- # When the defined procedure $name is called, it runs the supplied $body
- # within a new coroutine. The return value from the $name procedure call
- # will be a promise that will be fulfilled when the coroutine completes
- # normally or rejected if it completes with an error.
- #
- # Note that the passed $body argument is not the body of the
- # the procedure $name. Rather it is run as an anonymous procedure in
- # the coroutine but in the same namespace context as $name. Thus the
- # caller or the $body script must not make any assumptions about
- # relative stack levels, use of `uplevel` etc.
- #
- # The primary purpose of this command is to make it easy, in
- # conjunction with the [await] command, to wrap a sequence of asynchronous
- # operations as a single computational unit.
- #
- # Returns a promise that will be settled with the result of the script.
- if {![string equal -length 2 "$name" "::"]} {
- set ns [uplevel 1 namespace current]
- set name ${ns}::$name
- } else {
- set ns ::
- }
- set tmpl {
- proc %NAME% {%PARAMDEFS%} {
- set p [promise::Promise new [promise::lambda {real_args prom} {
- coroutine ::promise::async#[info cmdcount] {*}[promise::lambda {p args} {
- upvar #1 _current_async_promise current_p
- set current_p $p
- set status [catch [list apply [list {%PARAMDEFS%} {%BODY%} %NS%] {*}$args] res ropts]
- if {$status == 0} {
- $p fulfill $res
- } else {
- $p reject $res $ropts
- }
- } $prom {*}$real_args]
- } [lrange [info level 0] 1 end]]]
- return $p
- }
- }
- eval [string map [list %NAME% $name \
- %PARAMDEFS% $paramdefs \
- %BODY% $body \
- %NS% $ns] $tmpl]
-}
-
-proc promise::async_fulfill {val} {
- # Fulfills a promise for an async procedure with the specified value.
- # val - the value with which to fulfill the promise
- # This command must only be called with the context of an [async]
- # procedure.
- #
- # Returns an empty string.
- upvar #1 _current_async_promise current_p
- if {![info exists current_p]} {
- error "async_fulfill called from outside an async context."
- }
- $current_p fulfill $val
- return
-}
-
-proc promise::async_reject {val {edict {}}} {
- # Rejects a promise for an async procedure with the specified value.
- # val - the value with which to reject the promise
- # edict - error dictionary for rejection
- # This command must only be called with the context of an [async]
- # procedure.
- #
- # Returns an empty string.
- upvar #1 _current_async_promise current_p
- if {![info exists current_p]} {
- error "async_reject called from outside an async context."
- }
- $current_p reject $val $edict
- return
-}
-
-proc promise::async_chain {prom} {
- # Chains a promise for an async procedure to the specified promise.
- # prom - the promise to which the async promise is to be linked.
- # This command must only be called with the context of an [async]
- # procedure.
- #
- # Returns an empty string.
- upvar #1 _current_async_promise current_p
- if {![info exists current_p]} {
- error "async_chain called from outside an async context."
- }
- $current_p chain $prom
- return
-}
-
-proc promise::pfulfilled {value} {
- # Returns a new promise that is already fulfilled with the specified value.
- # value - the value with which to fulfill the created promise
- return [Promise new [lambda {value prom} {
- $prom fulfill $value
- } $value]]
-}
-
-proc promise::prejected {value {edict {}}} {
- # Returns a new promise that is already rejected.
- # value - the value with which to reject the promise
- # edict - error dictionary for rejection
- # By convention, $value should be of the format returned by
- # [Promise.reject].
- return [Promise new [lambda {value edict prom} {
- $prom reject $value $edict
- } $value $edict]]
-}
-
-proc promise::eventloop {prom} {
- # Waits in the eventloop until the specified promise is settled.
- # prom - the promise to be waited on
- # The command enters the event loop in similar fashion to the
- # Tcl `vwait` command except that instead of waiting on a variable
- # the command waits for the specified promise to be settled. As such
- # it has the same caveats as the vwait command in terms of care
- # being taken in nested calls etc.
- #
- # The primary use of the command is at the top level of a script
- # to wait for one or more promise based tasks to be completed. Again,
- # similar to the vwait forever idiom.
- #
- #
- # Returns the resolved value of $prom if it is fulfilled or raises an error
- # if it is rejected.
-
- set varname [namespace current]::_pwait_[info cmdcount]
- $prom done \
- [lambda {varname result} {
- set $varname [list success $result]
- } $varname] \
- [lambda {varname error ropts} {
- set $varname [list fail $error $ropts]
- } $varname]
- vwait $varname
- lassign [set $varname] status result ropts
- if {$status eq "success"} {
- return $result
- } else {
- return -options $ropts $result
- }
-}
-
-proc promise::pgeturl {url args} {
- # Returns a promise that will be fulfilled when the URL is fetched.
- # url - the URL to fetch
- # args - arguments to pass to the `http::geturl` command
- # This command invokes the asynchronous form of the `http::geturl` command
- # of the `http` package. If the operation completes with a status of
- # `ok`, the returned promise is fulfilled with the contents of the
- # http state array (see the documentation of `http::geturl`). If the
- # the status is anything else, the promise is rejected with
- # the `reason` parameter to the reaction containing the error message
- # and the `edict` parameter containing the Tcl error dictionary
- # with an additional key `http_state`, containing the
- # contents of the http state array.
-
- uplevel #0 {package require http}
- proc pgeturl {url args} {
- set prom [Promise new [lambda {http_args prom} {
- http::geturl {*}$http_args -command [promise::lambda {prom tok} {
- upvar #0 $tok http_state
- if {$http_state(status) eq "ok"} {
- $prom fulfill [array get http_state]
- } else {
- if {[info exists http_state(error)]} {
- set msg [lindex $http_state(error) 0]
- }
- if {![info exists msg] || $msg eq ""} {
- set msg "Error retrieving URL."
- }
- catch {throw {PROMISE PGETURL} $msg} msg edict
- dict set edict http_state [array get http_state]
- $prom reject $msg $edict
- }
- http::cleanup $tok
- } $prom]
- } [linsert $args 0 $url]]]
- return $prom
- }
- tailcall pgeturl $url {*}$args
-}
-
-proc promise::ptimer {millisecs {value "Timer expired."}} {
- # Returns a promise that will be fulfilled when the specified time has
- # elapsed.
- # millisecs - time interval in milliseconds
- # value - the value with which the promise is to be fulfilled
- # In case of errors (e.g. if $milliseconds is not an integer), the
- # promise is rejected with the `reason` parameter set to an error
- # message and the `edict` parameter set to a Tcl error dictionary.
- #
- # Also see [ptimeout] which is similar but rejects the promise instead
- # of fulfilling it.
-
- return [Promise new [lambda {millisecs value prom} {
- if {![string is integer -strict $millisecs]} {
- # We don't allow "idle", "cancel" etc. as an argument to after
- throw {PROMISE TIMER INVALID} "Invalid timeout value \"$millisecs\"."
- }
- after $millisecs [list promise::safe_fulfill $prom $value]
- } $millisecs $value]]
-}
-
-proc promise::ptimeout {millisecs {value "Operation timed out."}} {
- # Returns a promise that will be rejected when the specified time has
- # elapsed.
- # millisecs - time interval in milliseconds
- # value - the value with which the promise is to be rejected
- # In case of errors (e.g. if $milliseconds is not an integer), the
- # promise is rejected with the `reason` parameter set to $value
- # and the `edict` parameter set to a Tcl error dictionary.
- #
- # Also see [ptimer] which is similar but fulfills the promise instead
- # of rejecting it.
-
- return [Promise new [lambda {millisecs value prom} {
- if {![string is integer -strict $millisecs]} {
- # We don't want to accept "idle", "cancel" etc. for after
- throw {PROMISE TIMER INVALID} "Invalid timeout value \"$millisecs\"."
- }
- after $millisecs [::promise::lambda {prom msg} {
- catch {throw {PROMISE TIMER EXPIRED} $msg} msg edict
- ::promise::safe_reject $prom $msg $edict
- } $prom $value]
- } $millisecs $value]]
-}
-
-proc promise::pconnect {args} {
- # Returns a promise that will be fulfilled when the socket connection
- # is completed.
- # args - arguments to be passed to the Tcl `socket` command
- # This is a wrapper for the async version of the Tcl `socket` command.
- # If the connection completes, the promise is fulfilled with the
- # socket handle.
- # In case of errors (e.g. if the address cannot be fulfilled), the
- # promise is rejected with the `reason` parameter containing the
- # error message and the `edict` parameter containing the Tcl error
- # dictionary.
- #
- return [Promise new [lambda {so_args prom} {
- set so [socket -async {*}$so_args]
- fileevent $so writable [promise::lambda {prom so} {
- fileevent $so writable {}
- set err [chan configure $so -error]
- if {$err eq ""} {
- $prom fulfill $so
- } else {
- catch {throw {PROMISE PCONNECT FAIL} $err} err edict
- $prom reject $err $edict
- }
- } $prom $so]
- } $args]]
-}
-
-proc promise::_read_channel {prom chan data} {
- set newdata [read $chan]
- if {[string length $newdata] || ![eof $chan]} {
- append data $newdata
- fileevent $chan readable [list [namespace current]::_read_channel $prom $chan $data]
- return
- }
-
- # EOF
- set code [catch {
- # Need to make the channel blocking else no error is returned
- # on the close
- fileevent $chan readable {}
- fconfigure $chan -blocking 1
- close $chan
- } result edict]
- if {$code} {
- safe_reject $prom $result $edict
- } else {
- safe_fulfill $prom $data
- }
-}
-
-proc promise::pexec {args} {
- # Runs an external program and returns a promise for its output.
- # args - program and its arguments as passed to the Tcl `open` call
- # for creating pipes
- # If the program runs without errors, the promise is fulfilled by its
- # standard output content. Otherwise
- # promise is rejected.
- #
- # Returns a promise that will be settled by the result of the program
- return [Promise new [lambda {open_args prom} {
- set chan [open |$open_args r]
- fconfigure $chan -blocking 0
- fileevent $chan readable [list promise::_read_channel $prom $chan ""]
- } $args]]
-}
-
-proc promise::safe_fulfill {prom value} {
- # Fulfills the specified promise.
- # prom - the [Promise] object to be fulfilled
- # value - the fulfillment value
- # This is a convenience command that checks if $prom still exists
- # and if so fulfills it with $value.
- #
- # Returns 0 if the promise does not exist any more, else the return
- # value from its [fulfill][Promise.fulfill] method.
- if {![info object isa object $prom]} {
- # The object has been deleted. Naught to do
- return 0
- }
- return [$prom fulfill $value]
-}
-
-proc promise::safe_reject {prom value {edict {}}} {
- # Rejects the specified promise.
- # prom - the [Promise] object to be fulfilled
- # value - see [Promise.reject]
- # edict - see [Promise.reject]
- # This is a convenience command that checks if $prom still exists
- # and if so rejects it with the specified arguments.
- #
- # Returns 0 if the promise does not exist any more, else the return
- # value from its [reject][Promise.reject] method.
- if {![info object isa object $prom]} {
- # The object has been deleted. Naught to do
- return
- }
- $prom reject $value $edict
-}
-
-proc promise::ptask {script} {
- # Creates a new Tcl thread to run the specified script and returns
- # a promise for the script results.
- # script - script to run in the thread
- # Returns a promise that will be settled by the result of the script
- #
- # The `ptask` command runs the specified script in a new Tcl
- # thread. The promise returned from this command will be fulfilled
- # with the result of the script if it completes
- # successfully. Otherwise, the promise will be rejected with an
- # with the `reason` parameter containing the error message
- # and the `edict` parameter containing the Tcl error dictionary
- # from the script failure.
- #
- # Note that $script is a standalone script in that it is executed
- # in a new thread with a virgin Tcl interpreter. Any packages used
- # by $script have to be explicitly loaded, variables defined in the
- # the current interpreter will not be available in $script and so on.
- #
- # The command requires the Thread package to be loaded.
-
- uplevel #0 package require Thread
- proc [namespace current]::ptask script {
- return [Promise new [lambda {script prom} {
- set thread_script [string map [list %PROM% $prom %TID% [thread::id] %SCRIPT% $script] {
- set retcode [catch {%SCRIPT%} result edict]
- if {$retcode == 0 || $retcode == 2} {
- # ok or return
- set response [list ::promise::safe_fulfill %PROM% $result]
- } else {
- set response [list ::promise::safe_reject %PROM% $result $edict]
- }
- thread::send -async %TID% $response
- }]
- thread::create $thread_script
- } $script]]
- }
- tailcall [namespace current]::ptask $script
-}
-
-proc promise::pworker {tpool script} {
- # Runs a script in a worker thread from a thread pool and
- # returns a promise for the same.
- # tpool - thread pool identifier
- # script - script to run in the worker thread
- # Returns a promise that will be settled by the result of the script
- #
- # The Thread package allows creation of a thread pool with the
- # `tpool create` command. The `pworker` command runs the specified
- # script in a worker thread from a thread pool. The promise
- # returned from this command will be fulfilled with the result of
- # the script if it completes successfully.
- # Otherwise, the promise will be rejected with an
- # with the `reason` parameter containing the error message
- # and the `edict` parameter containing the Tcl error dictionary
- # from the script failure.
- #
- # Note that $script is a standalone script in that it is executed
- # in a new thread with a virgin Tcl interpreter. Any packages used
- # by $script have to be explicitly loaded, variables defined in the
- # the current interpreter will not be available in $script and so on.
-
- # No need for package require Thread since if tpool is passed to
- # us, Thread must already be loaded
- return [Promise new [lambda {tpool script prom} {
- set thread_script [string map [list %PROM% $prom %TID% [thread::id] %SCRIPT% $script] {
- set retcode [catch {%SCRIPT%} result edict]
- if {$retcode == 0 || $retcode == 2} {
- set response [list ::promise::safe_fulfill %PROM% $result]
- } else {
- set response [list ::promise::safe_reject %PROM% $result $edict]
- }
- thread::send -async %TID% $response
- }]
- tpool::post -detached -nowait $tpool $thread_script
- } $tpool $script]]
-}
-
-if {0} {
- package require http
- proc checkurl {url} {
- set prom [promise::Promise new [promise::lambda {url prom} {
- http::geturl $url -method HEAD -command [promise::lambda {prom tok} {
- upvar #0 $tok http_state
- $prom fulfill [list $http_state(url) $http_state(status)]
- ::http::cleanup $tok
- } $prom]
- } $url]]
- return $prom
- }
-
- proc checkurls {urls} {
- return [promise::all [lmap url $urls {checkurl $url}]]
- }
-
- [promise::all [
- list [
- promise::ptask {expr 1+1}
- ] [
- promise::ptask {expr 2+2}
- ]
- ]] done [promise::lambda val {puts [tcl::mathop::* {*}$val]}]
-}
-
-package provide promise [promise::version]
-
-if {[info exists ::argv0] &&
- [file tail [info script]] eq [file tail $::argv0]} {
- set filename [file tail [info script]]
- if {[llength $::argv] == 0} {
- puts "Usage: [file tail [info nameofexecutable]] $::argv0 dist|install|tm|version"
- exit 1
- }
- switch -glob -- [lindex $::argv 0] {
- ver* { puts [promise::version] }
- tm -
- dist* {
- if {[file extension $filename] ne ".tm"} {
- set dir [file join [file dirname [info script]] .. build]
- file mkdir $dir
- file copy -force [info script] [file join $dir [file rootname $filename]-[promise::version].tm]
- } else {
- error "Cannot create distribution from a .tm file"
- }
- }
- install {
- # Install in first native file system that exists on search path
- foreach path [tcl::tm::path list] {
- if {[lindex [file system $path] 0] eq "native"} {
- set dir $path
- if {[file isdirectory $path]} {
- break
- }
- # Else keep looking
- }
- }
- if {![file exists $dir]} {
- file mkdir $dir
- }
- if {[file extension $filename] eq ".tm"} {
- # We already are a .tm with version number
- set target $filename
- } else {
- set target [file rootname $filename]-[promise::version].tm
- }
- file copy -force [info script] [file join $dir $target]
- }
- default {
- puts stderr "Unknown option/command \"[lindex $::argv 0]\""
- exit 1
- }
- }
-}
diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm
deleted file mode 100644
index 55408253..00000000
--- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm
+++ /dev/null
@@ -1,8388 +0,0 @@
-#Punk - where radical modification is a craft and anti-patterns are another exploratory tool for the Pattern Punk.
-#Built on Tcl of course - because it's the most powerful piece of under-appreciated and alternate-thinking engineering you can plug into.
-
-
-namespace eval punk {
- proc lazyload {pkg} {
- package require zzzload
- if {[package provide $pkg] eq ""} {
- zzzload::pkg_require $pkg
- }
- }
- #lazyload twapi ?
-
- catch {package require vfs} ;#attempt load now so we can use faster 'package provide' to test existence later
-
- variable can_exec_windowsapp
- set can_exec_windowsapp unknown ;#don't spend a potential X00ms testing until needed
- variable windowsappdir
- set windowsappdir ""
- variable cmdexedir
- set cmdexedir ""
-
- proc sync_package_paths_script {} {
- #the tcl::tm namespace doesn't exist until one of the tcl::tm commands
- #is run. (they are loaded via ::auto_index triggering load of tm.tcl)
- #we call tcl::tm::list to trigger the initial set of tm paths before
- #we can override it, otherwise our changes will be lost
- #REVIEW - won't work on safebase interp where paths are mapped to {$p(:x:)} etc
- return "\
- apply {{ap tmlist} {
- set ::auto_path \$ap
- tcl::tm::list
- set ::tcl::tm::paths \$tmlist
- }} {$::auto_path} {[tcl::tm::list]}
- "
- }
-
- proc rehash {{refresh 0}} {
- global auto_execs
- if {!$refresh} {
- unset -nocomplain auto_execs
- } else {
- set names [array names auto_execs]
- unset -nocomplain auto_execs
- foreach nm $names {
- auto_execok_windows $nm
- }
- }
- return
- }
-
-
- proc ::punk::auto_execok_original name [info body ::auto_execok]
- variable better_autoexec
-
- #set better_autoexec 0 ;#use this var via better_autoexec only
- #proc ::punk::auto_execok_windows name {
- # ::punk::auto_execok_original $name
- #}
-
- set better_autoexec 1
- proc ::punk::auto_execok_windows name {
- ::punk::auto_execok_better $name
- }
-
- set has_commandstack [expr {![catch {package require commandstack}]}]
- if {$has_commandstack} {
- if {[catch {
- package require punk::packagepreference
- } errM]} {
- catch {puts stderr "Failed to load punk::packagepreference"}
- }
- catch punk::packagepreference::install
- } else {
- #
- }
-
- if {![interp issafe] && $has_commandstack && $::tcl_platform(platform) eq "windows"} {
-
- #still a caching version of auto_execok - but with proper(fixed) search order
-
- #set b [info body ::auto_execok]
- #proc ::auto_execok_original name $b
-
- proc better_autoexec {{onoff ""}} {
- variable better_autoexec
- if {$onoff eq ""} {
- return $better_autoexec
- }
- if {![string is boolean -strict $onoff]} {
- error "better_autoexec argument 'onoff' must be a boolean, received: $onoff"
- }
- if {$onoff && ($onoff != $better_autoexec)} {
- puts "Turning on better_autoexec - search PATH first then extension"
- set better_autoexec 1
- proc ::punk::auto_execok_windows name {
- ::punk::auto_execok_better $name
- }
- punk::rehash
- } elseif {!$onoff && ($onoff != $better_autoexec)} {
- puts "Turning off better_autoexec - search extension then PATH"
- set better_autoexec 0
- proc ::punk::auto_execok_windows name {
- ::punk::auto_execok_original $name
- }
- punk::rehash
- } else {
- puts "no change"
- }
- }
- #better_autoexec $better_autoexec ;#init to default
-
-
- proc auto_execok_better name {
- global auto_execs env tcl_platform
-
- if {[info exists auto_execs($name)]} {
- return $auto_execs($name)
- }
- #puts stdout "[a+ red]...[a]"
- set auto_execs($name) ""
-
- set shellBuiltins [list assoc cls copy date del dir echo erase exit ftype \
- md mkdir mklink move rd ren rename rmdir start time type ver vol]
- if {[info exists env(PATHEXT)]} {
- # Add an initial ; to have the {} extension check first.
- set execExtensions [split ";$env(PATHEXT)" ";"]
- } else {
- set execExtensions [list {} .com .exe .bat .cmd]
- }
-
- if {[string tolower $name] in $shellBuiltins} {
- # When this is command.com for some reason on Win2K, Tcl won't
- # exec it unless the case is right, which this corrects. COMSPEC
- # may not point to a real file, so do the check.
- set cmd $env(COMSPEC)
- if {[file exists $cmd]} {
- set cmd [file attributes $cmd -shortname]
- }
- return [set auto_execs($name) [list $cmd /c $name]]
- }
-
- if {[llength [file split $name]] != 1} {
- #has a path
- foreach ext $execExtensions {
- set file ${name}${ext}
- if {[file exists $file] && ![file isdirectory $file]} {
- return [set auto_execs($name) [list $file]]
- }
- }
- return ""
- }
-
- #change1
- #set path "[file dirname [info nameofexecutable]];.;"
- set path "[file dirname [info nameofexecutable]];"
-
- if {[info exists env(SystemRoot)]} {
- set windir $env(SystemRoot)
- } elseif {[info exists env(WINDIR)]} {
- set windir $env(WINDIR)
- }
- if {[info exists windir]} {
- append path "$windir/system32;$windir/system;$windir;"
- }
-
- foreach var {PATH Path path} {
- if {[info exists env($var)]} {
- append path ";$env($var)"
- }
- }
-
- #change2
- if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} {
- set lookfor [list $name]
- } else {
- set lookfor [lmap ext $execExtensions {string cat ${name} ${ext}}]
- }
- #puts "-->$lookfor"
- foreach dir [split $path {;}] {
- set dir [string trim $dir {\\}] ;#trailing slash will result in a tail such as "/python.exe"
- #set dir [file normalize $dir]
- # Skip already checked directories
- if {[info exists checked($dir)] || ($dir eq "")} {
- continue
- }
- set checked($dir) {}
-
- #surprisingly fast
- #set matches [glob -nocomplain -dir $dir -types f -tails {*}$lookfor]
- ##puts "--dir $dir matches:$matches"
- #if {[llength $matches]} {
- # set file [file join $dir [lindex $matches 0]]
- # #puts "--match0:[lindex $matches 0] file:$file"
- # return [set auto_execs($name) [list $file]]
- #}
-
- #what if it's a link?
- #foreach match [glob -nocomplain -dir $dir -types f -tail {*}$lookfor] {
- # set file [file join $dir $match]
- # if {[file exists $file]} {
- # return [set auto_execs($name) [list $file]]
- # }
- #}
-
- #safest? could be a link?
- foreach match [glob -nocomplain -dir $dir -tail {*}$lookfor] {
- set file [file join $dir $match]
- if {[file exists $file] && ![file isdirectory $file]} {
- return [set auto_execs($name) [list $file]]
- }
- }
- }
-
- #foreach ext $execExtensions {
- #unset -nocomplain checked
- #foreach dir [split $path {;}] {
- # # Skip already checked directories
- # if {[info exists checked($dir)] || ($dir eq "")} {
- # continue
- # }
- # set checked($dir) {}
- # set file [file join $dir ${name}${ext}]
- # if {[file exists $file] && ![file isdirectory $file]} {
- # return [set auto_execs($name) [list $file]]
- # }
- #}
- #}
- return ""
- }
-
-
-
- #review - what if punk package reloaded - but ::auto_execs has updated path for winget.exe?
- #what if we create another interp and use the same ::auto_execs? The appdir won't be detected.
- #TODO - see if there is a proper windows way to determine where the 'reparse point' apps are installed
-
-
-
- #winget is installed on all modern windows (but not on windows sandbox!) and is an example of the problem this addresses
- #we target apps with same location
-
- #the main purpose of this override is to support windows app executables (installed as 'reparse points')
- #for Tcl versions prior to the 2025-01 fix by APN https://core.tcl-lang.org/tcl/tktview/4f0b5767ac
- #versions prior to this will use cmd.exe to resolve the links
- set stackrecord [commandstack::rename_command -renamer ::punk auto_execok name {
- #set windowsappdir "%appdir%"
- upvar ::punk::can_exec_windowsapp can_exec_windowsapp
- upvar ::punk::windowsappdir windowsappdir
- upvar ::punk::cmdexedir cmdexedir
-
- if {$windowsappdir eq ""} {
- #we are targeting the winget location under the presumption this is where microsoft store apps are stored as 'reparse points'
- #Tcl (2025) can't exec when given a path to these 0KB files
- #This path is probably something like C:/Users/username/AppData/Local/Microsoft/WindowsApps
- if {!([info exists ::env(LOCALAPPDATA)] &&
- [file exists [set testapp [file join $::env(LOCALAPPDATA) "Microsoft" "WindowsApps" "winget.exe"]]])} {
- #should be unlikely to get here - unless LOCALAPPDATA missing (or winget.exe missing e.g windows sandbox)
- set windowsappdir [file dirname [lindex [::punk::auto_execok_windows winget.exe] 0]]
- catch {puts stderr "(resolved winget by search)"}
- } else {
- set windowsappdir [file dirname $testapp]
- }
- }
-
- #set default_auto [$COMMANDSTACKNEXT $name]
- set default_auto [::punk::auto_execok_windows $name]
- #if {$name ni {cmd cmd.exe}} {
- # unset -nocomplain ::auto_execs
- #}
-
- if {$default_auto eq ""} {
- return
- }
- set namedir [file dirname [lindex $default_auto 0]]
-
- if {$namedir eq $windowsappdir} {
- if {$can_exec_windowsapp eq "unknown"} {
- if {[catch {exec [file join $windowsappdir winget.exe] --version}]} {
- set can_exec_windowsapp 0
- } else {
- set can_exec_windowsapp 1
- }
- }
- if {$can_exec_windowsapp} {
- return [file join $windowsappdir $name]
- }
- if {$cmdexedir eq ""} {
- #cmd.exe very unlikely to move
- set cmdexedir [file dirname [lindex [::punk::auto_execok_windows cmd.exe] 0]]
- #auto_reset never seems to exist as a command - because auto_reset deletes all commands in the ::auto_index
- #anyway.. it has other side effects (affects auto_load)
- }
- return "[file join $cmdexedir cmd.exe] /c $name"
- }
- return $default_auto
- }]
-
-
- }
-
-}
-
-
-
-#repltelemetry cooperation with other packages such as shellrun
-#Maintenance warning: shellrun expects repltelemetry_emmitters to exist if punk namespace exists
-namespace eval punk {
- variable repltelemetry_emmitters
- #don't stomp.. even if something created this namespace in advance and is 'cooperating' a bit early
- if {![info exists repltelemetry_emitters]} {
- set repltelemetry_emmitters [list]
- }
-}
-
-namespace eval punk::pipecmds {
- #where to install proc/compilation artifacts for pieplines
- namespace export *
-}
-namespace eval punk::pipecmds::split_patterns {}
-namespace eval punk::pipecmds::split_rhs {}
-namespace eval punk::pipecmds::var_classify {}
-namespace eval punk::pipecmds::destructure {}
-namespace eval punk::pipecmds::insertion {}
-
-
-#globals... some minimal global var pollution
-#punk's official silly test dictionary
-set punk_testd [dict create \
- a0 a0val \
- b0 [dict create \
- a1 b0a1val \
- b1 b0b1val \
- c1 b0c1val \
- d1 b0d1val \
- ] \
- c0 [dict create] \
- d0 [dict create \
- a1 [dict create \
- a2 d0a1a2val \
- b2 d0a1b2val \
- c2 d0a1c2val \
- ] \
- b1 [dict create \
- a2 [dict create \
- a3 d0b1a2a3val \
- b3 d0b1a2b3val \
- ] \
- b2 [dict create \
- a3 d0b1b2a3val \
- bananas "in pyjamas" \
- c3 [dict create \
- po "in { }" \
- b4 ""\
- c4 "can go boom" \
- ] \
- d3 [dict create \
- a4 "-paper -cuts" \
- ] \
- e3 [dict create] \
- ] \
- ] \
- ] \
- e0 "multi\nline"\
- ]
-#test dict 2 - uniform structure and some keys with common prefixes for glob matching
-set punk_testd2 [dict create \
- a0 [dict create \
- b1 {a b c}\
- b2 {a b c d}\
- x1 {x y z 1 2}\
- y2 {X Y Z 1 2}\
- z1 {k1 v1 k2 v2 k3 v3}\
- ] \
- a1 [dict create \
- b1 {a b c}\
- b2 {a b c d}\
- x1 {x y z 1 2}\
- y2 {X Y Z 1 2}\
- z1 {k1 v1 k2 v2 k3 v3}\
- ] \
- b1 [dict create \
- b1 {a b c}\
- b2 {a b c d}\
- x1 {x y z 1 2}\
- y2 {X Y Z 1 2}\
- z1 {k1 v1 k2 v2 k3 v3}\
- ] \
-]
-
-#impolitely cooperative with punk repl - todo - tone it down.
-#namespace eval ::punk::repl::codethread {
-# variable running 0
-#}
-package require punk::lib ;# subdependency punk::args
-package require punk::ansi
-if {![llength [info commands ::ansistring]]} {
- namespace import punk::ansi::ansistring
-}
-#require aliascore after punk::lib & punk::ansi are loaded
-package require punk::aliascore ;#mostly punk::lib aliases
-punk::aliascore::init -force 1
-
-package require punk::repl::codethread
-package require punk::config
-#package require textblock
-package require punk::console ;#requires Thread
-package require punk::ns
-package require punk::winpath ;# for windows paths - but has functions that can be called on unix systems
-package require punk::repo
-package require punk::du
-package require punk::mix::base
-package require base64
-
-package require punk::pipe
-
-namespace eval punk {
- # -- --- ---
- #namespace import ::control::assert ;#according to tcllib doc - assert can be enabled/disabled per namespace
- # using control::control assert enabled within a namespace for which ::control::assert wasn't imported can produce surprising results.
- #e.g setting to zero may keep asserts enabled - (e.g if the assert command is still available due to namespace path etc) - but.. querying the enabled status may show zero even in the parent namespace where asserts also still work.
- #package require control
- #control::control assert enabled 1
-
- #We will use punk::assertion instead
-
- package require punk::assertion
- if {[catch {namespace import ::punk::assertion::assert} errM]} {
- catch {
- puts stderr "punk error importing punk::assertion::assert\n$errM"
- puts stderr "punk::a* commands:[info commands ::punk::a*]"
- }
- }
- punk::assertion::active on
- # -- --- ---
-
- interp alias {} purelist {} lreplace x 0 0 ;#required by pipe system
- if {[catch {
- package require pattern
- } errpkg]} {
- catch {puts stderr "Failed to load package pattern error: $errpkg"}
- }
- package require shellfilter
- package require punkapp
- package require funcl
-
- package require struct::list
- package require fileutil
- #package require punk::lib
-
- #NOTE - always call debug.xxx with braced message instead of double-quoted (unless specifically intending to do double-subtition)
- #(or $ within values will be substituted, causing an extra error message if the var doesn't exist - which it quite possibly doesn't)
- package require debug
-
- debug define punk.unknown
- debug define punk.pipe
- debug define punk.pipe.var
- debug define punk.pipe.args
- debug define punk.pipe.rep ;#string/list representation with tcl::unsupported::representation
- debug define punk.pipe.compile ;#info about when we compile pipeline components into procs etc
-
-
- #-----------------------------------
- # todo - load initial debug state from config
- debug off punk.unknown
- debug level punk.unknown 1
- debug off punk.pipe
- debug level punk.pipe 4
- debug off punk.pipe.var
- debug level punk.pipe.var 4
- debug off punk.pipe.args
- debug level punk.pipe.args 3
- debug off punk.pipe.rep 2
- debug off punk.pipe.compile
- debug level punk.pipe.compile 2
-
-
- debug header "dbg> "
-
-
- variable last_run_display [list]
-
-
- #variable re_headvar1 {([a-zA-Z:@.(),]+?)(?![^(]*\))(,.*)*$}
-
-
-
- #-----------------------------------------------------------------------------------
- #strlen is important for testing issues with string representationa and shimmering.
- #This specific implementation with append (as at 2023-09) is designed to ensure the original str representation isn't changed
- #It may need to be reviewed with different Tcl versions in case the append empty string is 'optimised/tuned' in some way that affects the behaviour
- proc strlen {str} {
- append str2 $str {}
- string length $str2
- }
- #-----------------------------------------------------------------------------------
-
- #get a copy of the item without affecting internal rep
- proc objclone {obj} {
- append obj2 $obj {}
- }
- proc set_clone {varname obj} {
- #maintenance: also punk::lib::set_clone
- #e.g used by repl's codeinterp. Maintains internal rep, easier to call e.g interp eval code [list punk::set_clone varnmame $val]
- append obj2 $obj {}
- uplevel 1 [list set $varname $obj2]
- }
-
- interp alias "" strlen "" ::punk::strlen
- interp alias "" str_len "" ::punk::strlen
- interp alias "" objclone "" ::punk::objclone
- #proc ::strlen {str} {
- # string length [append str2 $str {}]
- #}
- #proc ::objclone {obj} {
- # append obj2 $obj {}
- #}
-
- #-----------------------------------------------------------------------------------
- #order of arguments designed for pipelining
- #review - 'piper_' prefix is a naming convention for functions that are ordered for tail-argument pipelining
- #piper_ function names should read intuitively when used in a pipeline with tail argument supplied by the pipeline - but may seem reversed when using standalone.
- proc piper_append {new base} {
- append base $new
- }
- interp alias "" piper_append "" ::punk::piper_append
- proc piper_prepend {new base} {
- append new $base
- }
- interp alias "" piper_prepend "" ::punk::piper_prepend
-
- proc ::punk::K {x y} { return $x}
-
- #todo ansigrep? e.g grep using ansistripped value
- proc grepstr1 {pattern data} {
- set data [string map {\r\n \n} $data]
- set lines [split $data \n]
- set matches [lsearch -all -regexp $lines $pattern]
- set max [lindex $matches end]
- set w1 [string length $max]
- set result ""
- set H [a+ green bold overline]
- set R \x1b\[m
- foreach m $matches {
- set ln [lindex $lines $m]
- set ln [regsub -all $pattern $ln $H&$R]
- append result [format %${w1}s $m] " $ln" \n
- }
- set result [string trimright $result \n]
- return $result
- }
-
- #----------------------
- #todo - fix overtype
- #create test
- #overtype::renderline -insert_mode 0 -transparent 1 [a+ green]-----[a] " [a+ underline]x[a]"
- #----------------------
-
-
- punk::args::define {
- @id -id ::punk::grepstr
- @cmd -name punk::grepstr\
- -summary\
- "Grep for regex pattern in plaintext of supplied (possibly ANSI) string."\
- -help\
- "The grepstr command can find strings in ANSI text even if there are interspersed
- ANSI colour codes etc. Even if a word has different coloured/styled letters, the
- regex can match the plaintext. (Search is performed on ansistripped text, and then
- the matched sections are highlighted and overlayed on the original styled/colourd
- input.
- If the input string has ANSI movement codes - the resultant text may not be directly
- searchable because the parts of a word may be separated by various codes and other
- plain text. To search such an input string, the string should first be 'rendered' to
- a form where the ANSI only represents SGR styling (and perhaps other non-movement
- codes) using something like overtype::renderline or overtype::rendertext."
-
- @leaders -min 0 -max 0
- @opts
- -returnlines -type string -typesynopsis matched|all -default breaksandmatches -choicecolumns 1 -choices {matched all breaksandmatches} -choicelabels {
- "matched"\
- " Return only lines that matched."
- "breaksandmatches"\
- " Return configured --break= lines in between non-consecutive matches"
- "all"\
- " Return all lines.
- This has a similar effect to the 'grep' trick of matching on 'pattern|$'
- (The $ matches all lines that have an end; ie all lines, but there is no
- associated character to which to apply highlighting)
- except that when instead using -returnlines all with --line-number, the *
- indicator after the linenumber will only be highlighted for lines with matches,
- and the following matchcount will indicate zero for non-matching lines."
- }
- -B|--before-context= -parsekey "--before-context" -default 0 -type integer -typesynopsis num
- -C|--context= -parsekey "--context" -default 0 -type integer -typesynopsis num -help\
- "Print num lines of leading and trailing context surrounding each match."
- -A|--after-context= -parsekey "--after-context" -default 0 -type integer -typesynopsis num
- --break= -type string -default "-- %c%\U2260" -help\
- "When returning matched lines and there is a break in consecutive output,
- display the break with the given string. %c% is a placeholder for the
- number of lines skipped.
- Use empty-string for an empty line as a break display.
- grepstr --break= needle $haystacklines
-
- The unix grep utility commonly uses -- for this indicator.
- grepstr --break=-- needle $haystacklines
-
- Customisation example:
- grepstr -n \"--break=(skipped %c% lines)\" needle $haystacklines
- "
- -ansistrip -type none -help\
- "Strip all ansi codes from the input string before processing.
- This is not necessary for regex matching purposes, as the matching is always
- performed on the ansistripped characters anyway, but by stripping ANSI, the
- result only has the ANSI supplied by the -highlight option."
-
- #-n|--line-number as per grep utility, except that we include a * for matches
- -n|--line-number -type none -help\
- "Each output line is preceded by its relative line number in the file, starting at line 1.
- For lines that matched the regex, the line number will be suffixed with a * indicator
- with the same highlighting as the matched string(s).
- The number of matches in the line immediately follows the *
- For lines with no matches the * indicator is present with no highlighting and suffixed
- with zeros."
- -i|--ignore-case -type none -help\
- "Perform case insensitive matching."
- -highlight -type list -typesynopsis ansinames -default {green bold Black underline overline} -help\
- "list of ANSI SGR style codes as supported by and documented in punk::ansi::a?"
- -- -type none
- @values
- pattern -type string -help\
- "regex pattern to match in plaintext portion of ANSI string"
- string -type string
- }
- proc grepstr {args} {
- lassign [dict values [punk::args::parse $args withid ::punk::grepstr]] leaders opts values received
- set pattern [dict get $values pattern]
- set data [dict get $values string]
- set do_strip 0
- if {[dict exists $received -ansistrip]} {
- set data [punk::ansi::ansistrip $data]
- }
- set highlight [dict get $opts -highlight]
- set opt_returnlines [dict get $opts -returnlines]
- set context [dict get $opts --context] ;#int
- set beforecontext [dict get $opts --before-context]
- set beforecontext [expr {max($beforecontext,$context)}]
- set aftercontext [dict get $opts --after-context]
- set aftercontext [expr {max($aftercontext,$context)}]
- set break [dict get $opts --break]
- set ignorecase [dict exists $received --ignore-case]
- if {$ignorecase} {
- set nocase "-nocase"
- } else {
- set nocase ""
- }
-
-
- if {[dict exists $received --line-number]} {
- set do_linenums 1 ;#display lineindex+1
- } else {
- set do_linenums 0
- }
-
- if {[llength $highlight] == 0} {
- set H ""
- set R ""
- } else {
- set H [a+ {*}$highlight]
- set R \x1b\[m
- }
-
- set data [string map {\r\n \n} $data]
- if {![punk::ansi::ta::detect $data]} {
- set lines [split $data \n]
- set matches [lsearch -all {*}$nocase -regexp $lines $pattern]
- set result ""
- if {$opt_returnlines eq "all"} {
- set returnlines [punk::lib::range 0 [llength $lines]-1]
- } else {
- #matches|breaksandmatches
- set returnlines $matches
- }
- set max [lindex $returnlines end]
- if {[string is integer -strict $max]} {
- incr max
- }
- set w1 [string length $max]
- #lineindex is zero based - display of linenums is 1 based
- set resultlines [dict create]
- foreach lineindex $returnlines {
- set ln [lindex $lines $lineindex]
- set col1 ""
- if {$do_linenums} {
- set col1 [format "%${w1}s " [expr {$lineindex+1}]]
- }
- if {$lineindex in $matches} {
- set ln [regsub -all {*}$nocase -- $pattern $ln $H&$R] ;#n
- set matchcount [regexp -all {*}$nocase -- $pattern $ln]
- if {$do_linenums} {
- append col1 $H*$R[format %03s $matchcount]
- }
- } else {
- if {$do_linenums} {
- append col1 "*000"
- }
- }
- #---------------------------------------------------------------
- set prelines [lrange $lines $lineindex-$beforecontext $lineindex-1]
- set s [expr {$lineindex-$beforecontext-1}]
- if {$s < -1} {set s -1}
- foreach p $prelines {
- incr s
- #append result "[format %${w1}s [expr {$s+1}]]- " " " $p \n
- if {![dict exists $resultlines $s]} {
- if {$do_linenums} {
- set show "[format "%${w1}s " [expr {$s+1}]]- $p"
- } else {
- set show $p
- }
- dict set resultlines $s $show
- }
- }
- #---------------------------------------------------------------
- if {$do_linenums} {
- set show "$col1 $ln"
- } else {
- set show $ln
- }
- dict set resultlines $lineindex $show
- #---------------------------------------------------------------
- set postlines [lrange $lines $lineindex+1 $lineindex+$aftercontext]
- set s $lineindex
- foreach p $postlines {
- incr s
- if {![dict exists $resultlines $s]} {
- if {$do_linenums} {
- set show "[format "%${w1}s " [expr {$s+1}]]- $p"
- } else {
- set show $p
- }
- dict set resultlines $s $show
- }
- }
- #---------------------------------------------------------------
-
- }
- } else {
- set plain [punk::ansi::ansistrip $data]
- set plainlines [split $plain \n]
- set lines [split $data \n]
- set matches [lsearch -all {*}$nocase -regexp $plainlines $pattern]
- if {$opt_returnlines eq "all"} {
- set returnlines [punk::lib::range 0 [llength $lines]-1]
- } else {
- set returnlines $matches
- }
- set max [lindex $returnlines end]
- if {[string is integer -strict $max]} {
- #if max index is 9 - linenum will be 10, (99->100 etc) - so add one in case we're on such a boundary.
- incr max
- }
- set w1 [string length $max]
- set result ""
- set placeholder \UFFEF ;#review
- set resultlines [dict create]
- foreach lineindex $returnlines {
- set ln [lindex $lines $lineindex]
- set col1 ""
- if {$do_linenums} {
- set col1 [format "%${w1}s " [expr {$lineindex+1}]]
- }
- if {$lineindex in $matches} {
- set plain_ln [lindex $plainlines $lineindex]
- set parts [regexp -all {*}$nocase -indices -inline -- $pattern $plain_ln]
- set matchcount [llength $parts]
- if {$do_linenums} {
- append col1 $H*$R[format %03s $matchcount]
- }
- if {[llength $parts] == 0} {
- #This probably can't happen (?)
- #If it does.. it's more likely to be an issue with our line index than with regexp
- puts stderr "Unexpected regex mismatch in grepstr - line marked with ??? (shouldn't happen)"
- set matchshow "??? $ln"
- #dict set resultlines $lineindex $show
- } else {
- set overlay ""
- set i 0
- foreach prange $parts {
- lassign $prange s e
- set prelen [expr {$s - $i}]
- append overlay [string repeat $placeholder $prelen] $H[string range $plain_ln $s $e]$R
- set i [expr {$e + 1}]
- }
- set tail [string range $plain_ln $e+1 end]
- append overlay [string repeat $placeholder [string length $tail]]
- #puts "$overlay"
- #puts "$ln"
- set rendered [overtype::renderline -transparent $placeholder -insert_mode 0 $ln $overlay]
- if {$do_linenums} {
- set matchshow "$col1 $rendered"
- } else {
- set matchshow $rendered
- }
- }
- #---------------------------------------------------------------
- set prelines [lrange $lines $lineindex-$beforecontext $lineindex-1]
- set s [expr {$lineindex-$beforecontext-1}]
- if {$s < -1} {set s -1}
- foreach p $prelines {
- incr s
- #append result "[format %${w1}s [expr {$s+1}]]- " " " $p \n
- if {![dict exists $resultlines $s]} {
- if {$do_linenums} {
- set show "[format "%${w1}s " [expr {$s+1}]]- $p"
- } else {
- set show $p
- }
- dict set resultlines $s $show
- }
- }
- #---------------------------------------------------------------
- dict set resultlines $lineindex $matchshow
- #---------------------------------------------------------------
- set postlines [lrange $lines $lineindex+1 $lineindex+$aftercontext]
- set s $lineindex
- foreach p $postlines {
- incr s
- if {![dict exists $resultlines $s]} {
- if {$do_linenums} {
- set show "[format "%${w1}s " [expr {$s+1}]]- $p"
- } else {
- set show $p
- }
- dict set resultlines $s $show
- }
- }
- #---------------------------------------------------------------
- } else {
- if {$do_linenums} {
- append col1 "*000"
- set show "$col1 $ln"
- } else {
- set show $ln
- }
- dict set resultlines $lineindex $show
- }
- }
- }
- set ordered_resultlines [lsort -integer [dict keys $resultlines]]
- set result ""
- set i -1
- set do_break 0
- if {$opt_returnlines eq "breaksandmatches"} {
- set do_break 1
- }
- if {$do_break} {
- foreach r $ordered_resultlines {
- incr i
- if {$r > $i} {
- set c [expr {$r - $i}]
- append result [string map [list %c% $c] $break] \n
- }
- append result [dict get $resultlines $r] \n
- set i $r
- }
- if {$i<[llength $lines]-1} {
- set c [expr {[llength $lines]-1-$i}]
- append result [string map [list %c% $c] $break] \n
- }
- } else {
- foreach r $ordered_resultlines {
- append result [dict get $resultlines $r] \n
- }
- }
- set result [string trimright $result \n]
- return $result
- }
-
- proc stacktrace {} {
- set stack "Stack trace:\n"
- for {set i 1} {$i < [info level]} {incr i} {
- set lvl [info level -$i]
- set pname [lindex $lvl 0]
- append stack [string repeat " " $i]$pname
-
- if {![catch {info args $pname} pargs]} {
- foreach value [lrange $lvl 1 end] arg $pargs {
-
- if {$value eq ""} {
- if {$arg != 0} {
- info default $pname $arg value
- }
- }
- append stack " $arg='$value'"
- }
- } else {
- append stack " !unknown vars for $pname"
- }
-
- append stack \n
- }
- return $stack
- }
-
- #review - there are various type of uuid - we should use something consistent across platforms
- #twapi is used on windows because it's about 5 times faster - but is this more important than consistency?
- #twapi is much slower to load in the first place (e.g 75ms vs 6ms if package names already loaded) - so for oneshots tcllib uuid is better anyway
- #(counterpoint: in the case of punk - we currently need twapi anyway on windows)
- #does tcllib's uuid use the same mechanisms on different platforms anyway?
- proc ::punk::uuid {} {
- set has_twapi 0
- if 0 {
- if {"windows" eq $::tcl_platform(platform)} {
- if {![catch {
- set loader [zzzload::pkg_wait twapi]
- } errM]} {
- if {$loader in [list failed loading]} {
- catch {puts stderr "Unexpected problem during thread-load of pkg twapi - zzload::pkg_wait returned $loader"}
- }
- } else {
- package require twapi
- }
- if {[package provide twapi] ne ""} {
- set has_twapi 1
- }
- }
- }
- if {!$has_twapi} {
- if {[catch {package require uuid} errM]} {
- error "Unable to load a package for uuid on this platform. Try installing tcllib's uuid (any platform) - or twapi for windows"
- }
- return [uuid::uuid generate]
- } else {
- return [twapi::new_uuid]
- }
- }
-
- #get last command result that was run through the repl
- proc ::punk::get_runchunk {args} {
- set argd [punk::args::parse $args withdef {
- @id -id ::punk::get_runchunk
- @cmd -name "punk::get_runchunk" -help\
- "experimental"
- @opts
- -1 -optional 1 -type none
- -2 -optional 1 -type none
- @values -min 0 -max 0
- }]
- #todo - make this command run without truncating previous runchunks
- set runchunks [tsv::array names repl runchunks-*]
-
- set sortlist [list]
- foreach cname $runchunks {
- set num [lindex [split $cname -] 1]
- lappend sortlist [list $num $cname]
- }
- set sorted [lsort -index 0 -integer $sortlist]
- set chunkname [lindex $sorted end-1 1]
- set runlist [tsv::get repl $chunkname]
- #puts stderr "--$runlist"
- if {![llength $runlist]} {
- return ""
- } else {
- return [lindex [lsearch -inline -index 0 $runlist result] 1]
- }
- }
- interp alias {} _ {} ::punk::get_runchunk
-
-
- proc ::punk::var {varname {= _=.=_} args} {
- upvar $varname the_var
- switch -exact -- ${=} {
- = {
- if {[llength $args] > 1} {
- set the_var $args
- } else {
- set the_var [lindex $args 0]
- }
- }
- .= {
- if {[llength $args] > 1} {
- set the_var [uplevel 1 $args]
- } else {
- set the_var [uplevel 1 [lindex $args 0]]
- }
- }
- _=.=_ {
- set the_var
- }
- default {
- set the_var [list ${=} {*}$args]
- }
- }
- }
- proc src {args} {
- #based on wiki.. https://wiki.tcl-lang.org/page/source+with+args
- #added support for ?-encoding name? and other options of Tcl source command under assumption they come pairs before the filename
- # review? seems unlikely source command will ever accept solo options. It would make complete disambiguation impossible when passing additional args as we are doing here.
- set cmdargs [list]
- set scriptargs [list]
- set inopts 0
- set i 0
- foreach a $args {
- if {$i eq [llength $args]-1} {
- #reached end without finding end of opts
- #must be file - even if it does match -* ?
- break
- }
- if {!$inopts} {
- if {[string match -* $a]} {
- set inopts 1
- } else {
- #leave loop at first nonoption - i should be index of file
- break
- }
- } else {
- #leave for next iteration to check
- set inopts 0
- }
- incr i
- }
- set cmdargs [lrange $args 0 $i]
- set scriptargs [lrange $args $i+1 end]
- set argv $::argv
- set argc $::argc
- set ::argv $scriptargs
- set ::argc [llength $scriptargs]
- set code [catch {uplevel [list source {*}$cmdargs]} return]
- set ::argv $argv
- set ::argc $argc
- return -code $code $return
- }
-
-
-
-
- proc varinfo {vname {flag ""}} {
- upvar $vname v
- if {[array exists $vname]} {
- error "can't read \"$vname\": variable is array"
- }
- if {[catch {set v} err]} {
- error "can't read \"$vname\": no such variable"
- }
- set inf [shellfilter::list_element_info [list $v]]
- set inf [dict get $inf 0]
- if {$flag eq "-v"} {
- return $inf
- }
-
- set output [dict create]
- dict set output wouldbrace [dict get $inf wouldbrace]
- dict set output wouldescape [dict get $inf wouldescape]
- dict set output head_tail_names [dict get $inf head_tail_names]
- dict set output len [dict get $inf len]
- return $output
- }
-
- #review - extending core commands could be a bit intrusive...although it can make sense in a pipeline.
- #e.g contrived pipeline example to only allow setting existing keys
- ## .= @head.= list {a aaa b bbb c ccc} |d,dkeys@keys> |> &true.= {is_list_all_in_list $nkeys $dkeys} |> {dict modify d {*}$new} |> &true.= {is_list_all_ni_list $nkeys $dkeys} |> {dict modify d {*}$new} -1} {
- #lassign [punk::lib::string_splitbefore $token $first_term] v k
- set v [string range $token 0 $first_term-1]
- set k [string range $token $first_term end] ;#key section includes the terminal char
- lappend varlist [list $v $k]
- } else {
- lappend varlist [list $token ""]
- }
- set token ""
- set token_index -1 ;#reduce by 1 because , not included in next token
- set first_term -1
- } else {
- if {$first_term == -1} {
- if {$c in $var_terminals} {
- set first_term $token_index
- }
- }
- append token $c
- if {$c eq "("} {
- set in_brackets 1
- }
- }
- }
- incr token_index
- }
- if {[string length $token]} {
- if {$first_term > -1} {
- set v [string range $token 0 $first_term-1]
- set k [string range $token $first_term end] ;#key section includes the terminal char
- lappend varlist [list $v $k]
- } else {
- lappend varlist [list $token ""]
- }
- }
- return $varlist
- }
-
- proc fp_restructure {selector data} {
- if {$selector eq ""} {
- fun=.= {val $input} and always break
- set lhs ""
- set rhs ""
- #todo - check performance impact of catches around list and dict operations - consider single catch around destructure and less specific match error info?
- foreach index $subindices {
- set subpath [join [lrange $subindices 0 $i_keyindex] /]
- set lhs $subpath
- set assigned ""
- set get_not 0
- set already_assigned 0
- set do_bounds_check 0 ;#modified by leading single @ for list operations - doesn't apply to certain items like 'head','tail' which have specifically defined bounds-checks implicit in their normal meaning.
- #thse have anyhead and anytail for explicit allowance to be used on lists with insufficient items to produce values.
- #todo - see if 'string is list' improved in tcl9 vs catch {llength $list}
- switch -exact -- $index {
- # {
- set active_key_type "list"
- if {![catch {llength $leveldata} assigned]} {
- set already_assigned 1
- } else {
- set action ?mismatch-not-a-list
- break
- }
- }
- ## {
- set active_key_type "dict"
- if {![catch {dict size $leveldata} assigned]} {
- set already_assigned 1
- } else {
- set action ?mismatch-not-a-dict
- break
- }
- }
- #? {
- #review - compare to %# ?????
- #seems to be unimplemented ?
- set assigned [string length $leveldata]
- set already_assigned 1
- }
- @ {
- upvar v_list_idx v_list_idx ;#positional tracker for /@ - list position
- set active_key_type "list"
- #e.g @1/1/@/1 the lone @ is a positional spec for this specific subkey
- #no normalization done - ie @2/@ will not be considered same subkey as @end/@ or @end-0/@ even if llength = 3
- #while x@,y@.= is reasonably handy - especially for args e.g $len} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- set assigned [lindex $leveldata $index]
- set already_assigned 1
- }
- @@ - @?@ - @??@ {
- set active_key_type "dict"
-
- #NOTE: it may at first seem pointless to use @@/key, since we have to know the key - but this can be used to match 'key' only at the first position in .= list key {x y} key2 etc
- #x@@ = a {x y}
- #x@@/@0 = a
- #x@@/@1 = x y
- #x@@/a = a {x y}
- # but.. as the @@ is stateful - it generally isn't very useful for multiple operations on the same pair within the pattern group.
- # (note that ?@ forms a different subpath - so can be used to test match prior to @@ without affecting the index)
- # It is analogous to v1@,v2@ for lists.
- # @pairs is more useful for repeated operations
-
- #
- #set subpath [join [lrange $subindices 0 $i_keyindex] /]
- if {[catch {dict size $leveldata} dsize]} {
- set action ?mismatch-not-a-dict
- break
- }
- set next_this_level [incr v_dict_idx($subpath)]
- set keyindex [expr {$next_this_level -1}]
- if {($keyindex + 1) <= $dsize} {
- set k [lindex [dict keys $leveldata] $keyindex]
- if {$index eq "@?@"} {
- set assigned [dict get $leveldata $k]
- } else {
- set assigned [list $k [dict get $leveldata $k]]
- }
- } else {
- if {$index eq "@@"} {
- set action ?mismatch-dict-index-out-of-range
- break
- } else {
- set assigned [list]
- }
- }
- set already_assigned 1
- }
- default {
- switch -glob -- $index {
- @@* {
- set active_key_type "dict"
- set key [string range $index 2 end]
- #dict exists test is safe - no need for catch
- if {[dict exists $leveldata $key]} {
- set assigned [dict get $leveldata $key]
- } else {
- set action ?mismatch-dict-key-not-found
- break
- }
- set already_assigned 1
- }
- {@\?@*} {
- set active_key_type "dict"
- set key [string range $index 3 end]
- #dict exists test is safe - no need for catch
- if {[dict exists $leveldata $key]} {
- set assigned [dict get $leveldata $key]
- } else {
- set assigned [list]
- }
- set already_assigned 1
- }
- {@\?\?@*} {
- set active_key_type "dict"
- set key [string range $index 4 end]
- #dict exists test is safe - no need for catch
- if {[dict exists $leveldata $key]} {
- set assigned [list $key [dict get $leveldata $key]]
- } else {
- set assigned [list]
- }
- set already_assigned 1
- }
- @* {
- set active_key_type "list"
- set do_bounds_check 1
- set index [string trimleft $index @]
- }
- default {
- #
- }
- }
-
- if {!$already_assigned} {
- if {[string match "not-*" $index] && $active_key_type in [list "" "list"]} {
- #e.g not-0-end-1 not-end-4-end-2
- set get_not 1
- #cherry-pick some easy cases, and either assign, or re-map to corresponding index
- switch -- $index {
- not-tail {
- set active_key_type "list"
- set assigned [lindex $leveldata 0]; set already_assigned 1
- }
- not-head {
- set active_key_type "list"
- #set selector "tail"; set get_not 0
- set assigned [lrange $leveldata 1 end]; set already_assigned 1
- }
- not-end {
- set active_key_type "list"
- set assigned [lrange $leveldata 0 end-1]; set already_assigned 1
- }
- default {
- #trim off the not- and let the remaining index handle based on get_not being 1
- set index [string range $index 4 end]
- }
- }
- }
- }
- }
- }
-
- if {!$already_assigned} {
-
- #keyword 'pipesyntax' at beginning of error message
- set listmsg "pipesyntax Unable to interpret subindex $index\n"
- append listmsg "selector: '$selector'\n"
- append listmsg "@ must be followed by a selector (possibly compound separated by forward slashes) suitable for lindex or lrange commands, or a not-x expression\n"
- append listmsg "Additional accepted keywords include: head tail\n"
- append listmsg "Use var@@key to treat value as a dict and retrieve element at key"
-
-
- #we can't just set 'assigned' for a position spec for in/ni (not-in) because we don't have the value here to test against
- #need to set a corresponding action
- if {$active_key_type in [list "" "list"]} {
- set active_key_type "list"
- #for pattern matching purposes - head/tail not valid on empty lists (similar to elixir)
- if {$index eq "0"} {
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- set assigned [lindex $leveldata 0]
- } elseif {$index eq "head"} {
- #NOTE: /@head and /head both do bounds check. This is intentional
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- if {$len == 0} {
- set action ?mismatch-list-index-out-of-range-empty
- break
- }
- #alias for 0 - for h@head,t@tail= similar to erlang/elixir hd() tl() or [head | tail] = list syntax
- set assigned [lindex $leveldata 0]
- } elseif {$index eq "end"} {
- # @end /end
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- if {$do_bounds_check && $len < 1} {
- set action ?mismatch-list-index-out-of-range
- }
- set assigned [lindex $leveldata end]
- } elseif {$index eq "tail"} {
- #NOTE: /@tail and /tail both do bounds check. This is intentional.
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- #tail is a little different in that we allow tail on a single element list - returning an empty result - but it can't be called on an empty list
- #arguably tail could be considered as an index-out-of-range for less than 2 elements - but this would be less useful, and surprising to those coming from other pattern-matching systems.
- #In this way tail is different to @1-end
- if {$len == 0} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- set assigned [lrange $leveldata 1 end] ;#return zero or more elements - but only if there is something (a head) at position zero.
- } elseif {$index eq "anyhead"} {
- # @anyhead
- #allow returning of head or nothing if empty list
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- set assigned [lindex $leveldata 0]
- } elseif {$index eq "anytail"} {
- # @anytail
- #allow returning of tail or nothing if empty list
- #anytail will return empty both for empty list, or single element list - but potentially useful in combination with anyhead.
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- set assigned [lrange $leveldata 1 end]
- } elseif {$index eq "init"} {
- # @init
- #all but last element - same as haskell 'init'
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- set assigned [lrange $leveldata 0 end-1]
- } elseif {$index eq "list"} {
- # @list
- #allow returning of entire list even if empty
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- set assigned $leveldata
- } elseif {$index eq "raw"} {
- #no list checking..
- set assigned $leveldata
- } elseif {$index eq "keys"} {
- #need active_key_type of 'list' for 'keys' and 'values' keywords which act on either dict or a list with even number of elements
- if {[catch {dict size $leveldata} dsize]} {
- set action ?mismatch-not-a-dict
- break
- }
- set assigned [dict keys $leveldata]
- } elseif {$index eq "values"} {
- #need active_key_type of 'list' for 'keys' and 'values' keywords which act on either dict or a list with even number of elements
- if {[catch {dict size $leveldata} dsize]} {
- set action ?mismatch-not-a-dict
- break
- }
- set assigned [dict values $leveldata]
- } elseif {$index eq "pairs"} {
- if {[catch {dict size $leveldata} dsize]} {
- set action ?mismatch-not-a-dict
- break
- }
- #set assigned [dict values $leveldata]
- set pairs [list]
- tcl::dict::for {k v} $leveldata {lappend pairs [list $k $v]}
- set assigned [lindex [list $pairs [unset pairs]] 0]
- } elseif {[string is integer -strict $index]} {
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- # only check if @ was directly in original index section
- if {$do_bounds_check && ($index+1 > $len || $index < 0)} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- if {$get_not} {
- #already handled not-0
- set assigned [lreplace $leveldata $index $index]
- } else {
- set assigned [lindex $leveldata $index]
- }
- } elseif {[string first "end" $index] >=0} {
- if {[regexp {^end([-+]{1,2}[0-9]+)$} $index _match endspec]} {
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- #leave the - from the end- as part of the offset
- set offset [expr $endspec] ;#don't brace! (consider: set x --34;puts expr $j;puts expr {$j} )
- if {$do_bounds_check && ($offset > 0 || abs($offset) >= $len)} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- if {$get_not} {
- set assigned [lreplace $leveldata $index $index]
- } else {
- set assigned [lindex $leveldata $index]
- }
- } elseif {[regexp {^([0-9]+|end|end[-+]{1,2}[0-9]+)-([0-9]+|end|end[-+]{1,2}([0-9]+))$} $index _ start end]} {
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- if {$do_bounds_check && [string is integer -strict $start]} {
- if {$start+1 > $len || $start < 0} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- } elseif {$start eq "end"} {
- #ok
- } elseif {$do_bounds_check} {
- set startoffset [string range $start 3 end] ;#include the - from end-
- set startoffset [expr $startoffset] ;#don't brace!
- if {$startoffset > 0 || abs($startoffset) >= $len} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- }
- if {$do_bounds_check && [string is integer -strict $end]} {
- if {$end+1 > $len || $end < 0} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- } elseif {$end eq "end"} {
- #ok
- } elseif {$do_bounds_check} {
- set endoffset [string range $end 3 end] ;#include the - from end-
- set endoffset [expr $endoffset] ;#don't brace!
- if {$endoffset > 0 || abs($endoffset) >= $len} {
- set action ?mismatch-list-index-out-of-range
- break
- }
- }
- if {$get_not} {
- set assigned [lreplace $leveldata $start $end]
- } else {
- set assigned [lrange $leveldata $start $end]
- }
- } else {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- } elseif {[string first - $index] > 0} {
- puts "====> index:$index leveldata:$leveldata"
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- break
- }
- #handle pure int-int ranges separately
- set testindex [string map [list - "" + ""] $index]
- if {[string is digit -strict $testindex]} {
- #don't worry about leading - negative value for indices not valid anyway
- set parts [split $index -]
- if {[llength $parts] != 2} {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- lassign $parts start end
- if {$start+1 > $len || $end+1 > $len} {
- set action ?mismatch-not-a-list
- break
- }
- if {$get_not} {
- set assigned [lreplace $leveldata $start $end]
- } else {
- set assigned [lrange $leveldata $start $end]
- }
- } else {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
-
- } else {
- #keyword 'pipesyntax' at beginning of error message
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- } else {
- #treat as dict key
- set active_key_type "dict"
- if {[dict exists $leveldata $index]} {
- set assigned [dict get $leveldata $index]
- } else {
- set action ?mismatch-dict-key-not-found
- break
- }
-
- }
- }
- set leveldata $assigned
- set rhs $leveldata
- #don't break on empty data - operations such as # and ## can return 0
- #if {![llength $leveldata]} {
- # break
- #}
- incr i_keyindex
- }
- #puts stdout "----> destructure rep leveldata: [rep $leveldata]"
- #puts stdout ">> destructure returning: [dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs]"
-
- #maintain key order - caller unpacks using lassign
- return [dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs]
-
- }
- #todo - fp_destructure - return a function-pipeline that can then be transformed to a funcl and finally a more efficient tcl script
- proc destructure_func {selector data} {
- #puts stderr ".d."
- set selector [string trim $selector /]
- #upvar v_list_idx v_list_idx ;#positional tracker for /@ - list position
- #upvar v_dict_idx v_dict_idx ;#positional tracker for /@@ - dict position
-
- #map some problematic things out of the way in a manner that maintains some transparency
- #e.g glob chars ? * in a command name can make testing using {[info commands $cmd] ne ""} produce spurious results - requiring a stricter (and slower) test such as {$cmd in [info commands $cmd]}
- #The selector forms part of the proc name
- #review - compare with pipecmd_namemapping
- set selector_safe [string map [list\
- ? \
- * \
- \\ \
- {"} \
- {$} \
- "\x1b\[" \
- "\x1b\]" \
- {[} \
- {]} \
- :: \
- {;} \
- " " \
- \t \
- \n \
- \r \
- ] $selector]
-
- set cmdname ::punk::pipecmds::destructure::_$selector_safe
- if {[info commands $cmdname] ne ""} {
- return [$cmdname $data] ;# note upvar 2 for stateful v_list_idx to be resolved in _multi_bind_result context
- }
-
- set leveldata $data
- set body [destructure_func_build_procbody $cmdname $selector $data]
-
- puts stdout ----
- puts stderr "proc $cmdname {leveldata} {"
- puts stderr $body
- puts stderr "}"
- puts stdout ---
- proc $cmdname {leveldata} $body
- #eval $script ;#create the proc
- debug.punk.pipe.compile {proc $cmdname} 4
- #return [dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs]
- #use return - script has upvar 2 for v_list_idx to be resolved in _multi_bind_result context
- return [$cmdname $data]
- }
-
- #Builds a *basic* function to do the destructuring.
- #This is simply a set of steps to destructure each level of the data based on the hierarchical selector.
- #It just uses intermediate variables and adds some comments to the code to show the indices used at each point.
- #This may be useful in the long run as a debug/fallback mechanism - but ideally we should be building a more efficient script.
- proc destructure_func_build_procbody {cmdname selector data} {
- set script ""
- #place selector in comment in script only - if there is an error in selector we pick it up when building the script.
- #The script itself should only be returning errors in its action key of the result dictionary
- append script \n [string map [list $selector] {# set selector {}}]
- set subindices [split $selector /]
- append script \n [string map [list [list $subindices]] {# set subindices }]
- set action ?match ;#default assumption. Alternatively set to ?mismatch or ?mismatch- and always break
- append script \n {set action ?match}
- #append script \n {set assigned ""} ;#review
- set active_key_type ""
- append script \n {# set active_key_type ""}
- set lhs ""
- #append script \n [tstr {set lhs ${{$lhs}}}]
- append script \n {set lhs ""}
- set rhs ""
- append script \n {set rhs ""}
-
- set INDEX_OPERATIONS {} ;#caps to make clear in templates that this is substituted from script building scope
-
- #maintain key order - caller unpacks using lassign
- set returnline {dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs}
- set return_template {return [tcl::dict::create -assigned $leveldata -action $action -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- #set tpl_return_mismatch {return [dict create -assigned $leveldata -action ${$MISMATCH} -lhs $lhs -rhs $rhs -index_operations {${$INDEX_OPERATIONS}}]}
- set tpl_return_mismatch {return [dict create -assigned $leveldata -action ${$MISMATCH} -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- set tpl_return_mismatch_not_a_list {return [dict create -assigned $leveldata -action ?mismatch-not-a-list -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- set tpl_return_mismatch_list_index_out_of_range {return [dict create -assigned $leveldata -action ?mismatch-list-index-out-of-range -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- set tpl_return_mismatch_list_index_out_of_range_empty {return [dict create -assigned $leveldata -action ?mismatch-list-index-out-of-range-empty -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- set tpl_return_mismatch_not_a_dict {return [dict create -assigned $leveldata -action ?mismatch-not-a-dict -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- #dict 'index' when using stateful @@ etc to iterate over dict instead of by key
- set tpl_return_mismatch_dict_index_out_of_range {return [dict create -assigned $leveldata -action ?mismatch-dict-index-out-of-range -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
- set tpl_return_mismatch_dict_key_not_found {return [dict create -assigned $leveldata -action ?mismatch-dict-key-not-found -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]}
-
-
- if {![string length $selector]} {
- #just return $leveldata
- set script {
- dict create -assigned $leveldata -action ?match -lhs "" -rhs $leveldata
- }
- return $script
- }
-
- if {[string is digit -strict [join $subindices ""]]} {
- #review tip 551 (tcl9+?)
- #puts stderr ">>>>>>>>>>>>>>>> data: $leveldata selector: $selector subindices: $subindices"
- #pure numeric keylist - put straight to lindex
- #
- #NOTE: this direct access e.g v/0/1/2 doesn't check out of bounds which is at odds with list access containing @
- #We will leave this as a syntax for different (more performant) behaviour
- #- it's potentially a little confusing - but it would be a shame not to have the possibility to take advantage of the lindex deep indexing capability in pattern matching.
- #TODO - review and/or document
- #
- #Todo - add a handler for v/n/n/n/n/# to allow unchecked counting at depth too.
- #(or more generally - loop until we hit another type of subindex)
-
- #set assigned [lindex $leveldata {*}$subindices]
- if {[llength $subindices] == 1} {
- append script \n "# index_operation listindex" \n
- lappend INDEX_OPERATIONS listindex
- } else {
- append script \n "# index_operation listindex-nested" \n
- lappend INDEX_OPERATIONS listindex-nested
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {lindex $leveldata ${$subindices}} leveldata]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- }
- }]
- # -- --- ---
- #append script \n $returnline \n
- append script [tstr -return string $return_template]
- return $script
- # -- --- ---
- }
- if {[string match @@* $selector]} {
- #part following a double @ is dict key possibly with forward-slash separators for subpath access e.g @@key/subkey/etc
- set rawkeylist [split $selector /] ;#first key retains @@ - may be just '@@'
- set keypath [string range $selector 2 end]
- set keylist [split $keypath /]
- lappend INDEX_OPERATIONS dict_path
- if {([lindex $rawkeylist 0] ne "@@") && ([lsearch $keylist @*] == -1) && ([lsearch $keylist #*] == -1) && ([lsearch $keylist %*] == -1)} {
- #pure keylist for dict - process in one go
- #dict exists will return 0 if not a valid dict.
- # is equivalent to {*}keylist when substituted
- append script \n [tstr -return string -allowcommands {
- if {[dict exists $leveldata ${$keylist}]} {
- set leveldata [dict get $leveldata ${$keylist}]
- } else {
- #set action ?mismatch-dict-key-not-found
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
- append script [tstr -return string $return_template]
- return $script
- # -- --- ---
- }
- #else
- #compound keylist e.g x@@data/@0/1 or x@@/a (combined dict/list access)
- #process level by level
- }
-
-
-
- set i_keyindex 0
- append script \n {set i_keyindex 0}
- #todo - check performance impact of catches around list and dict operations - consider single catch around destructure and less specific match error info?
- foreach index $subindices {
- #set index_operation "unspecified"
- set level_script_complete 0 ;#instead of break - as we can't use data to determine break when building script
- set SUBPATH [join [lrange $subindices 0 $i_keyindex] /]
- append script \n "# ------- START index:$index subpath:$SUBPATH ------"
- set lhs $index
- append script \n "set lhs {$index}"
-
- set assigned ""
- append script \n {set assigned ""}
-
- #got_not shouldn't need to be in script
- set get_not 0
- if {[tcl::string::index $index 0] eq "!"} {
- append script \n {#get_not is true e.g !0-end-1 !end-4-end-2 !0 !@0 !@@key}
- set index [tcl::string::range $index 1 end]
- set get_not 1
- }
-
- # do_bounds_check shouldn't need to be in script
- set do_bounds_check 0 ;#modified by leading single @ for list operations - doesn't apply to certain items like 'head','tail' which have specifically defined bounds-checks implicit in their normal meaning.
- #thse have anyhead and anytail for explicit allowance to be used on lists with insufficient items to produce values.
- #append script \n {set do_boundscheck 0}
- switch -exact -- $index {
- # - @# {
- #list length
- set active_key_type "list"
- if {$get_not} {
- lappend INDEX_OPERATIONS not-list
- append script \n {# set active_key_type "list" index_operation: not-list}
- append script \n {
- if {[catch {llength $leveldata}]} {
- #not a list - not-length is true
- set assigned 1
- } else {
- #is a list - not-length is false
- set assigned 0
- }
- }
- } else {
- lappend INDEX_OPERATIONS list-length
- append script \n {# set active_key_type "list" index_operation: list-length}
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} assigned]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- }
- }]
- }
- set level_script_complete 1
- }
- ## {
- #dict size
- set active_key_type "dict"
- if {$get_not} {
- lappend INDEX_OPERATIONS not-dict
- append script \n {# set active_key_type "dict" index_operation: not-dict}
- append script \n {
- if {[catch {dict size $leveldata}]} {
- set assigned 1 ;#not a dict - not-size is true
- } else {
- set assigned 0 ;#is a dict - not-size is false
- }
- }
- } else {
- lappend INDEX_OPERATIONS dict-size
- append script \n {# set active_key_type "dict" index_operation: dict-size}
- append script \n [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} assigned]} {
- #set action ?mismatch-not-a-dict
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- }
- set level_script_complete 1
- }
- %# {
- set active_key_type "string"
- if {$get_not} {
- error "!%# not string length is not supported"
- }
- #string length - REVIEW -
- lappend INDEX_OPERATIONS string-length
- append script \n {# set active_key_type "" index_operation: string-length}
- append script \n {set assigned [string length $leveldata]}
- set level_script_complete 1
- }
- %%# {
- #experimental
- set active_key_type "string"
- if {$get_not} {
- error "!%%# not string length is not supported"
- }
- #string length - REVIEW -
- lappend INDEX_OPERATIONS ansistring-length
- append script \n {# set active_key_type "" index_operation: ansistring-length}
- append script \n {set assigned [ansistring length $leveldata]}
- set level_script_complete 1
- }
- %str {
- set active_key_type "string"
- if {$get_not} {
- error "!%str - not string-get is not supported"
- }
- lappend INDEX_OPERATIONS string-get
- append script \n {# set active_key_type "" index_operation: string-get}
- append script \n {set assigned $leveldata}
- set level_script_complete 1
- }
- %sp {
- #experimental
- set active_key_type "string"
- if {$get_not} {
- error "!%sp - not string-space is not supported"
- }
- lappend INDEX_OPERATIONS string-space
- append script \n {# set active_key_type "" index_operation: string-space}
- append script \n {set assigned " "}
- set level_script_complete 1
- }
- %empty {
- #experimental
- set active_key_type "string"
- if {$get_not} {
- error "!%empty - not string-empty is not supported"
- }
- lappend INDEX_OPERATIONS string-empty
- append script \n {# set active_key_type "" index_operation: string-empty}
- append script \n {set assigned ""}
- set level_script_complete 1
- }
- @words {
- set active_key_type "string"
- if {$get_not} {
- error "!%words - not list-words-from-string is not supported"
- }
- lappend INDEX_OPERATIONS list-words-from-string
- append script \n {# set active_key_type "" index_operation: list-words-from-string}
- append script \n {set assigned [regexp -inline -all {\S+} $leveldata]}
- set level_script_complete 1
- }
- @chars {
- #experimental - leading character based on result not input(?)
- #input type is string - but output is list
- set active_key_type "list"
- if {$get_not} {
- error "!%chars - not list-chars-from-string is not supported"
- }
- lappend INDEX_OPERATIONS list-from_chars
- append script \n {# set active_key_type "" index_operation: list-chars-from-string}
- append script \n {set assigned [split $leveldata ""]}
- set level_script_complete 1
- }
- @join {
- #experimental - flatten one level of list
- #join without arg - output is list
- set active_key_type "string"
- if {$get_not} {
- error "!@join - not list-join-list is not supported"
- }
- lappend INDEX_OPERATIONS list-join-list
- append script \n {# set active_key_type "" index_operation: list-join-list}
- append script \n {set assigned [join $leveldata]}
- set level_script_complete 1
- }
- %join {
- #experimental
- #input type is list - but output is string
- set active_key_type "string"
- if {$get_not} {
- error "!%join - not string-join-list is not supported"
- }
- lappend INDEX_OPERATIONS string-join-list
- append script \n {# set active_key_type "" index_operation: string-join-list}
- append script \n {set assigned [join $leveldata ""]}
- set level_script_complete 1
- }
- %ansiview {
- set active_key_type "string"
- if {$get_not} {
- error "!%# not string-ansiview is not supported"
- }
- lappend INDEX_OPERATIONS string-ansiview
- append script \n {# set active_key_type "" index_operation: string-ansiview}
- append script \n {set assigned [ansistring VIEW $leveldata]}
- set level_script_complete 1
- }
- %ansiviewstyle {
- set active_key_type "string"
- if {$get_not} {
- error "!%# not string-ansiviewstyle is not supported"
- }
- lappend INDEX_OPERATIONS string-ansiviewstyle
- append script \n {# set active_key_type "" index_operation: string-ansiviewstyle}
- append script \n {set assigned [ansistring VIEWSTYLE $leveldata]}
- set level_script_complete 1
- }
- @ {
- #as this is a stateful list next index operation - we use not (!@) to mean there is no element at the next index (instead of returning the complement ie all elements except next)
- #This is in contrast to other not operations on indices e.g /!2 which returns all elements except that at index 2
-
-
- #append script \n {puts stderr [uplevel 1 [list info vars]]}
-
- #NOTE:
- #v_list_idx in context of _multi_bind_result
- #we call destructure_func from _mult_bind_result which in turn calls the proc (or the script on first run)
- append script \n {upvar 2 v_list_idx v_list_idx}
-
- set active_key_type "list"
- append script \n {# set active_key_type "list" index_operation: list-get-next}
- #e.g @1/1/@/1 the lone @ is a positional spec for this specific subkey
- #no normalization done - ie @2/@ will not be considered same subkey as @end/@ or @end-0/@ even if llength = 3
- #while x@,y@.= is reasonably handy - especially for args e.g $len} {
- set assigned 1
- } else {
- set assigned 0
- }
- }]
-
- } else {
- lappend INDEX_OPERATIONS get-next
- append script \n [tstr -return string -allowcommands {
- set index [expr {[incr v_list_idx(@)]-1}]
-
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } elseif {$index+1 > $len} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- } else {
- set assigned [lindex $leveldata $index]
- }
- }]
- }
- set level_script_complete 1
- }
- @* {
- set active_key_type "list"
- if {$get_not} {
- lappend INDEX_OPERATIONS list-is-empty
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } elseif {$len == 0} {
- set assigned 1 ;#list is empty
- } else {
- set assigned 0
- }
- }]
- } else {
- lappend INDEX_OPERATIONS list-get-all
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- set assigned [lrange $leveldata 0 end]
- }
- }]
- }
- set level_script_complete 1
- }
- @@ {
- #stateful: tracking of index using v_dict_idx
- set active_key_type "dict"
- lappend INDEX_OPERATIONS get-next-value
- append script \n {# set active_key_type "dict" index_operation: get-next-value}
- append script \n {upvar v_dict_idx v_dict_idx} ;#review!
-
- #NOTE: it may at first seem pointless to use @@/key, since we have to know the key - but this can be used to match 'key' only at the first position in .= list key {x y} key2 etc
- #x@@ = a {x y}
- #x@@/@0 = a
- #x@@/@1 = x y
- #x@@/a = a {x y}
- # but.. as the @@ is stateful - it generally isn't very useful for multiple operations on the same pair within the pattern group.
- # (note that @@ @?@ @??@ form different subpaths - so the ? & ?? versions can be used to test match prior to @@ without affecting the index)
- #review - might be more useful if they shared an index ?
- # It is analogous to v1@,v2@ for lists.
- # @pairs is more useful for repeated operations
-
-
- set indent " "
- set assignment_script [string map [list \r\n "\r\n$indent" \n "\n$indent" ] {
- if {($keyindex + 1) <= $dsize} {
- set k [lindex [dict keys $leveldata] $keyindex]
- set assigned [list $k [dict get $leveldata $k]]
- } else {
- ${[tstr -ret string $tpl_return_mismatch_dict_index_out_of_range]}
- }
- }]
-
- set assignment_script [tstr -ret string -allowcommands $assignment_script]
-
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} dsize]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- } else {
- set next_this_level [incr v_dict_idx(${$SUBPATH})]
- set keyindex [expr {$next_this_level -1}]
- ${$assignment_script}
- }
- }]
- set level_script_complete 1
- }
- @?@ {
- #stateful: tracking of index using v_dict_idx
- set active_key_type "dict"
- lappend INDEX_OPERATIONS get?-next-value
- append script \n {# set active_key_type "dict" index_operation: get?-next-value}
- append script \n {upvar v_dict_idx v_dict_idx} ;#review!
- set indent " "
- set assignment_script [string map [list \r\n "\r\n$indent" \n "\n$indent" ] {
- if {($keyindex + 1) <= $dsize} {
- set k [lindex [dict keys $leveldata] $keyindex]
- set assigned [dict get $leveldata $k]
- } else {
- set assigned [list]
- }
- }]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} dsize]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- } else {
- set next_this_level [incr v_dict_idx(${$SUBPATH})]
- set keyindex [expr {$next_this_level -1}]
- ${$assignment_script}
- }
- }]
- set level_script_complete 1
- }
- @??@ {
- set active_key_type "dict"
- lappend INDEX_OPERATIONS get?-next-pair
- append script \n {# set active_key_type "dict" index_operation: get?-next-pair}
- append script \n {upvar v_dict_idx v_dict_idx} ;#review!
- set indent " "
- set assignment_script [string map [list \r\n "\r\n$indent" \n "\n$indent" ] {
- if {($keyindex + 1) <= $dsize} {
- set k [lindex [dict keys $leveldata] $keyindex]
- set assigned [list $k [dict get $leveldata $k]]
- } else {
- set assigned [list]
- }
- }]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} dsize]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- } else {
- set next_this_level [incr v_dict_idx(${$SUBPATH})]
- set keyindex [expr {$next_this_level -1}]
- ${$assignment_script}
- }
- }]
- set level_script_complete 1
- }
- @vv@ - @VV@ - @kk@ - @KK@ {
- error "unsupported index $index"
- }
- default {
-
- #assert rules for values within @@
- #glob search is done only if there is at least one * within @@
- #if there is at least one ? within @@ - then a non match will not raise an error (quiet)
-
- #single or no char between @@:
- #lookup/search is based on key - return is values
-
- #double char within @@:
- #anything with a dot returns k v pairs e.g @k.@ @v.@ @..@
- #anything that is a duplicate returns k v pairs e.g @kk@ @vv@ @**@
- #anything with a letter and a star returns the type of the letter, and the search is based on the position of the star where posn 1 is for key, posn 2 is for value
- #e.g @k*@ returns keys - search on values
- #e.g @*k@ returns keys - search on keys
- #e.g @v*@ returns values - search on values
- #e.g @*v@ returns values - search on keys
-
- switch -glob -- $index {
- @@* {
- #exact key match - return value
- #noisy get value - complain if key non-existent
- #doesn't complain if not a dict - because we use 'tcl::dict::exists' which will return false without error even if the value isn't dict-shaped
- set active_key_type "dict"
- set key [string range $index 2 end]
- if {$get_not} {
- lappend INDEX_OPERATIONS exactkey-get-value-not
- #review - dict remove allows silent call if key doesn't exist - but we are enforcing existence here
- #this seems reasonable given we have an explicit @?@ syntax (nocomplain equivalent) and there could be a legitimate case for wanting a non-match if trying to return the complement of a non-existent key
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict" index_operation: exactkey-get-value-not
- if {[dict exists $leveldata ${$key}]} {
- set assigned [dict values [dict remove $leveldata ${$key}]]
- } else {
- #set action ?mismatch-dict-key-not-found
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
-
- } else {
- lappend INDEX_OPERATIONS exactkey-get-value
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict index_operation: exactkey-get-value"
- if {[dict exists $leveldata ${$key}]} {
- set assigned [dict get $leveldata ${$key}]
- } else {
- #set action ?mismatch-dict-key-not-found
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
- }
- set level_script_complete 1
- }
- {@\?@*} {
- #exact key match - quiet get value
- #silent empty result if non-existent key - silence when non-existent key also if using not-@?@badkey which will just return whole dict
- #note - dict remove will raise error on non-dict-shaped value whilst dict exists will not
- set active_key_type "dict"
- set key [string range $index 3 end]
- if {$get_not} {
- lappend INDEX_OPERATIONS exactkey?-get-value-not
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict" index_operation: exactkey?-get-value-not
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- set assigned [dict values [dict remove $leveldata ${$key}]]
- }]
-
- } else {
- lappend INDEX_OPERATIONS exactkey?-get-value
- #dict exists test is safe - no need for catch
- append script \n [string map [list $key] {
- # set active_key_type "dict" index_operation: exactkey?-get-value
- if {[dict exists $leveldata ]} {
- set assigned [dict get $leveldata ]
- } else {
- set assigned [dict create]
- }
- }]
- }
- set level_script_complete 1
- }
- {@\?\?@*} {
- #quiet get pairs
- #this is silent too.. so how do we do a checked return of dict key+val?
- set active_key_type "dict"
- set key [string range $index 4 end]
- if {$get_not} {
- lappend INDEX_OPERATIONS exactkey?-get-pair-not
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict" index_operation: exactkey?-get-pair-not
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- set assigned [dict remove $leveldata ${$key}]
- }]
- } else {
- lappend INDEX_OPERATIONS exactkey?-get-pair
- append script \n [string map [list $key] {
- # set active_key_type "dict" index_operation: exactkey?-get-pair
- if {[dict exists $leveldata ]} {
- set assigned [dict create [dict get $leveldata ]]
- } else {
- set assigned [dict create]
- }
- }]
- }
- set level_script_complete 1
- }
- @..@* - @kk@* - @KK@* {
- #noisy get pairs by key
- set active_key_type "dict"
- set key [string range $index 4 end]
- if {$get_not} {
- lappend INDEX_OPERATIONS exactkey-get-pairs-not
- #review - dict remove allows silent call if key doesn't exist - but we are enforcing existence here
- #this seems reasonable given we have an explicit @?@ syntax (nocomplain equivalent) and there could be a legitimate case for wanting a non-match if trying to return the complement of a non-existent key
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict" index_operation: exactkey-get-pairs-not
- if {[dict exists $leveldata ${$key}]} {
- set assigned [tcl::dict::remove $leveldata ${$key}]
- } else {
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
-
- } else {
- lappend INDEX_OPERATIONS exactkey-get-pairs
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict index_operation: exactkey-get-pairs"
- if {[dict exists $leveldata ${$key}]} {
- tcl::dict::set assigned ${$key} [tcl::dict::get $leveldata ${$key}]
- } else {
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
- }
- set level_script_complete 1
-
- }
- @vv@* - @VV@* {
- #noisy(?) get pairs by exact value
- #return mismatch on non-match even when not- specified
- set active_key_type "dict"
- set keyglob [string range $index 4 end]
- set active_key_type "dict"
- set key [string range $index 4 end]
- if {$get_not} {
- #review - for consistency we are reporting a mismatch when the antikey being looked up doesn't exist
- #The utility of this is debatable
- lappend INDEX_OPERATIONS exactvalue-get-pairs-not
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict" index_operation: exactvalue-get-pairs-not
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- set nonmatches [dict create]
- tcl::dict::for {k v} $leveldata {
- if {![string equal ${$key} $v]} {
- dict set nonmatches $k $v
- }
- }
-
- if {[dict size $nonmatches] < [dict size $leveldata]} {
- #our key matched something
- set assigned $nonmatches
- } else {
- #our key didn't match anything - don't return the nonmatches
- #set action ?mismatch-dict-key-not-found
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
-
- } else {
- lappend INDEX_OPERATIONS exactvalue-get-pairs
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict index_operation: exactvalue-get-pairs-not"
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- set matches [list]
- tcl::dict::for {k v} $leveldata {
- if {[string equal ${$key} $v]} {
- lappend matches $k $v
- }
- }
- if {[llength $matches]} {
- set assigned $matches
- } else {
- #set action ?mismatch-dict-key-not-found
- ${[tstr -ret string $tpl_return_mismatch_dict_key_not_found]}
- }
- }]
- }
- set level_script_complete 1
-
- }
- {@\*@*} - {@\*v@*} - {@\*V@*} {
- #dict key glob - return values only
- set active_key_type "dict"
- if {[string match {@\*@*} $index]} {
- set keyglob [string range $index 3 end]
- } else {
- #vV
- set keyglob [string range $index 4 end]
- }
- #if $keyglob eq "" - needs to query for dict key that is empty string.
- if {$get_not} {
- lappend INDEX_OPERATIONS globkey-get-values-not
- append script \n [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- # set active_key_type "dict" index_operation: globkey-get-values-not
- set matched [dict keys $leveldata {${$keyglob}}]
- set assigned [dict values [dict remove $leveldata {*}$matched]]
- }]
-
- } else {
- lappend INDEX_OPERATIONS globkey-get-values
- append script \n [tstr -return string -allowcommands {
- # set active_key_type "dict" index_operation: globkey-get-values
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- set matched [dict keys $leveldata {${$keyglob}}]
- set assigned [list]
- foreach m $matched {
- lappend assigned [dict get $leveldata $m]
- }
- }]
- }
- set level_script_complete 1
-
- }
- {@\*.@*} {
- #dict key glob - return pairs
- set active_key_type "dict"
- set keyglob [string range $index 4 end]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- if {$get_not} {
- lappend INDEX_OPERATIONS globkey-get-pairs-not
- append script \n [string map [list $keyglob] {
- # set active_key_type "dict" index_operation: globkey-get-pairs-not
- set matched [dict keys $leveldata {}]
- set assigned [dict remove $leveldata {*}$matched]
- }]
-
- } else {
- lappend INDEX_OPERATIONS globkey-get-pairs
- append script \n [string map [list $keyglob] {
- # set active_key_type "dict" index_operations: globkey-get-pairs
- set matched [dict keys $leveldata {}]
- set assigned [dict create]
- foreach m $matched {
- dict set assigned $m [dict get $leveldata $m]
- }
- }]
- }
- set level_script_complete 1
- }
- {@\*k@*} - {@\*K@*} {
- #dict key glob - return keys
- set active_key_type "dict"
- set keyglob [string range $index 4 end]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- if {$get_not} {
- lappend INDEX_OPERATIONS globkey-get-keys-not
- append script \n [string map [list $keyglob] {
- # set active_key_type "dict" index_operation: globkey-get-keys-not
- set matched [dict keys $leveldata {}]
- set assigned [dict keys [dict remove $leveldata {*}$matched]]
- }]
-
- } else {
- lappend INDEX_OPERATIONS globkey-get-keys
- append script \n [string map [list $keyglob] {
- # set active_key_type "dict" index_operation: globkey-get-keys
- set assigned [dict keys $leveldata {}]
- }]
- }
- set level_script_complete 1
- }
- {@k\*@*} - {@K\*@*} {
- #dict value glob - return keys
- set active_key_type "dict"
- set valglob [string range $index 4 end]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- if {$get_not} {
- lappend INDEX_OPERATIONS globvalue-get-keys-not
- append script \n [string map [list $valglob] {
- # set active_key_type "dict" index_operation: globvalue-get-keys-not
- set assigned [list]
- tcl::dict::for {k v} $leveldata {
- if {![string match {} $v]} {
- lappend assigned $k
- }
- }
- }]
- } else {
- lappend INDEX_OPERATIONS globvalue-get-keys
- append script \n [string map [list $valglob] {
- # set active_key_type "dict" index_operation: globvalue-get-keys
- set assigned [list]
- tcl::dict::for {k v} $leveldata {
- if {[string match {} $v]} {
- lappend assigned $k
- }
- }
- }]
- }
- set level_script_complete 1
- }
- {@.\*@*} {
- #dict value glob - return pairs
- set active_key_type "dict"
- set valglob [string range $index 4 end]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- if {$get_not} {
- lappend INDEX_OPERATIONS globvalue-get-pairs-not
- append script \n [string map [list $valglob] {
- # set active_key_type "dict" index_operation: globvalue-get-pairs-not
- set assigned [dict create]
- tcl::dict::for {k v} $leveldata {
- if {![string match {} $v]} {
- dict set assigned $k $v
- }
- }
- }]
- } else {
- lappend INDEX_OPERATIONS globvalue-get-pairs
- append script \n [string map [list $valglob] {
- # set active_key_type "dict" index_operation: globvalue-get-pairs
- set assigned [dict create]
- tcl::dict::for {k v} $leveldata {
- if {[string match {} $v]} {
- dict set assigned $k $v
- }
- }
- }]
- }
- set level_script_complete 1
- }
- {@V\*@*} - {@v\*@*} {
- #dict value glob - return values
- set active_key_type dict
- set valglob [string range $index 4 end]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- if {$get_not} {
- lappend INDEX_OPERATIONS globvalue-get-values-not
- append script \n [string map [list $valglob] {
- # set active_key_type "dict" ;# index_operation: globvalue-get-values-not
- set assigned [list]
- tcl::dict::for {k v} $leveldata {
- if {![string match {} $v]} {
- lappend assigned $v
- }
- }
- }]
-
- } else {
- lappend INDEX_OPERATIONS globvalue-get-values
- append script \n [string map [list $valglob] {
- # set active_key_type "dict" ;#index_operation: globvalue-get-value
- set assigned [dict values $leveldata ]
- }]
- }
- set level_script_complete 1
-
- }
- {@\*\*@*} {
- #dict val/key glob return pairs)
- set active_key_type "dict"
- set keyvalglob [string range $index 4 end]
- append script [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata}]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- }
- }]
- if {$get_not} {
- lappend INDEX_OPERATIONS globkeyvalue-get-pairs-not
- error "globkeyvalue-get-pairs-not todo"
- } else {
- lappend INDEX_OPERATIONS globkeyvalue-get-pairs
- append script \n [string map [list $keyvalglob] {
- # set active_key_type "dict" ;# index_operation: globkeyvalue-get-pairs-not
- set assigned [dict create]
- tcl::dict::for {k v} $leveldata {
- if {[string match {} $k] || [string match {} $v]} {
- dict set assigned $k $v
- }
- }
- }]
- }
- set level_script_complete 1
- puts stderr "globkeyvalue-get-pairs review"
- }
- @* {
- set active_key_type "list"
- set do_bounds_check 1
-
- set index [string trimleft $index @]
- append script \n [string map [list $index] {
- # set active_key_type "list" index_operation: ?
- set index
- }]
- }
- %* {
- set active_key_type "string"
- set do_bounds_check 0
- set index [string range $index 1 end]
- append script \n [string map [list $index] {
- # set active_key_type "string" index_operation: ?
- set index
- }]
- }
- default {
- puts "destructure_func_build_body unmatched index $index"
- }
- }
- }
- }
-
- if {!$level_script_complete} {
-
-
- #keyword 'pipesyntax' at beginning of error message
- set listmsg "pipesyntax Unable to interpret subindex $index\n"
- append listmsg "selector: '$selector'\n"
- append listmsg "@ must be followed by a selector (possibly compound separated by forward slashes) suitable for lindex or lrange commands, or a not-x expression\n"
- append listmsg "Additional accepted keywords include: head tail\n"
- append listmsg "Use var@@key to treat value as a dict and retrieve element at key"
-
- #append script \n [string map [list $listmsg] {set listmsg ""}]
-
-
-
- #we can't just set 'assigned' for a position spec for in/ni (not-in) because we don't have the value here to test against
- #need to set a corresponding action
- if {$active_key_type in [list "" "list"]} {
- set active_key_type "list"
- append script \n {# set active_key_type "list"}
- #for pattern matching purposes - head/tail not valid on empty lists (similar to elixir)
- switch -exact -- $index {
- 0 {
- if {$get_not} {
- append script \n "# index_operation listindex-int-not" \n
- lappend INDEX_OPERATIONS listindex-zero-not
- set assignment_script {set assigned [lrange $leveldata 1 end]}
- } else {
- lappend INDEX_OPERATIONS listindex-zero
- set assignment_script {set assigned [lindex $leveldata 0]}
- if {$do_bounds_check} {
- append script \n "# index_operation listindex-int (bounds checked)" \n
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } elseif {[llength $leveldata] == 0} {
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]}
- } else {
- ${$assignment_script}
- }
- }]
- } else {
- append script \n "# index_operation listindex-int" \n
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- }
- }
- head {
- #NOTE: /@head and /head both do bounds check. This is intentional
- if {$get_not} {
- append script \n "# index_operation listindex-head-not" \n
- lappend INDEX_OPERATIONS listindex-head-not
- set assignment_script {set assigned [lrange $leveldata 1 end]}
- } else {
- append script \n "# index_operation listindex-head" \n
- lappend INDEX_OPERATIONS listindex-head
- set assignment_script {set assigned [lindex $leveldata 0]}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } elseif {$len == 0} {
- #set action ?mismatch-list-index-out-of-range-empty
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]}
- } else {
- #alias for 0 - for h@head,t@tail= similar to erlang/elixir hd() tl() or [head | tail] = list syntax
- ${$assignment_script}
- }
- }]
- }
- end {
- if {$get_not} {
- append script \n "# index_operation listindex-end-not" \n
- lappend INDEX_OPERATIONS listindex-end-not
- #on single element list Tcl's lrange will do what we want here and return nothing
- set assignment_script {set assigned [lrange $leveldata 0 end-1]}
- } else {
- append script \n "# index_operation listindex-end" \n
- lappend INDEX_OPERATIONS listindex-end
- set assignment_script {set assigned [lindex $leveldata end]}
- }
- if {$do_bounds_check} {
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } elseif {$len == 0} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]}
- } else {
- ${$assignment_script}
- }
- }]
- } else {
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- }
- tail {
- #NOTE: /@tail and /tail both do bounds check. This is intentional.
- #
- #tail is a little different in that we allow tail on a single element list - returning an empty result - but it can't be called on an empty list
- #arguably tail could be considered as an index-out-of-range for less than 2 elements - but this would be less useful, and surprising to those coming from other pattern-matching systems.
- #In this way tail is different to @1-end
- if {$get_not} {
- append script \n "# index_operation listindex-tail-not" \n
- lappend INDEX_OPERATIONS listindex-tail-not
- set assignment_script {set assigned [lindex $leveldata 0]}
- } else {
- append script \n "# index_operation listindex-tail" \n
- lappend INDEX_OPERATIONS listindex-tail
- set assignment_script {set assigned [lrange $leveldata 1 end] ;#return zero or more elements - but only if there is something (a head) at position zero}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } elseif {$len == 0} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- anyhead {
- #allow returning of head or nothing if empty list
- if {$get_not} {
- append script \n "# index_operation listindex-anyhead-not" \n
- lappend INDEX_OPERATIONS listindex-anyhead-not
- set assignment_script {set assigned [lrange $leveldata 1 end]}
- } else {
- append script \n "# index_operation listindex-anyhead" \n
- lappend INDEX_OPERATIONS listindex-anyhead
- set assignment_script {set assigned [lindex $leveldata 0]}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- anytail {
- #allow returning of tail or nothing if empty list
- #anytail will return empty both for empty list, or single element list - but potentially useful in combination with anyhead.
- if {$get_not} {
- append script \n "# index_operation listindex-anytail-not" \n
- lappend INDEX_OPERATIONS listindex-anytail-not
- set assignment_script {set assigned [lindex $leveldata 0]}
- } else {
- append script \n "# index_operation listindex-anytail" \n
- lappend INDEX_OPERATIONS listindex-anytail
- set assignment_script {set assigned [lrange $leveldata 1 end]}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- init {
- #all but last element - same as haskell 'init'
- #counterintuitively, get-notinit can therefore return first element if it is a single element list
- #does bounds_check for get-not@init make sense here? maybe - review
- if {$get_not} {
- append script \n "# index_operation listindex-init-not" \n
- lappend INDEX_OPERATIONS listindex-init-not
- set assignment_script {set assigned [lindex $leveldata end]}
- } else {
- append script \n "# index_operation listindex-init" \n
- lappend INDEX_OPERATIONS listindex-init
- set assignment_script {set assigned [lrange $leveldata 0 end-1]}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- list {
- #get_not?
- #allow returning of entire list even if empty
- if {$get_not} {
- lappend INDEX_OPERATIONS list-getall-not
- set assignment_script {set assigned {}}
- } else {
- lappend INDEX_OPERATIONS list-getall
- set assignment_script {set assigned $leveldata}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- raw {
- #get_not - return nothing??
- #no list checking..
- if {$get_not} {
- lappend INDEX_OPERATIONS getraw-not
- append script \n {set assigned {}}
- } else {
- lappend INDEX_OPERATIONS getraw
- append script \n {set assigned $leveldata}
- }
- }
- keys {
- #@get_not??
- #need active_key_type of 'list' for 'keys' and 'values' keywords which act on either dict or a list with even number of elements
- if {$get_not} {
- lappend INDEX_OPERATIONS list-getkeys-not
- set assignment_script {set assigned [dict values $leveldata]} ;#not-keys is equivalent to values
- } else {
- lappend INDEX_OPERATIONS list-getkeys
- set assignment_script {set assigned [dict keys $leveldata]}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} dsize]} {
- #set action ?mismatch-not-a-dict
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- values {
- #get_not ??
- #need active_key_type of 'list' for 'keys' and 'values' keywords which act on either dict or a list with even number of elements
- if {$get_not} {
- lappend INDEX_OPERATIONS list-getvalues-not
- set assignment_script {set assigned [dict keys $leveldata]} ;#not-values is equivalent to keys
- } else {
- lappend INDEX_OPERATIONS list-getvalues
- set assignment_script {set assigned [dict values $leveldata]}
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} dsize]} {
- #set action ?mismatch-not-a-dict
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- } else {
- ${$assignment_script}
- }
- }]
- }
- pairs {
- #get_not ??
- if {$get_not} {
- #review - return empty list instead like not-list and not-raw?
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector not-pairs_not_supported]
- } else {
- lappend INDEX_OPERATIONS list-getpairs
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {dict size $leveldata} dsize]} {
- #set action ?mismatch-not-a-dict
- ${[tstr -ret string $tpl_return_mismatch_not_a_dict]}
- } else {
- set pairs [list]
- tcl::dict::for {k v} $leveldata {lappend pairs [list $k $v]}
- set assigned [lindex [list $pairs [unset pairs]] 0]
- }
- }]
- }
- default {
- if {[regexp {[?*]} $index]} {
- if {$get_not} {
- lappend INDEX_OPERATIONS listsearch-not
- set assign_script [string map [list $index] {
- set assigned [lsearch -all -inline -not $leveldata ]
- }]
- } else {
- lappend INDEX_OPERATIONS listsearch
- set assign_script [string map [list $index] {
- set assigned [lsearch -all -inline $leveldata ]
- }]
- }
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assign_script}
- }
- }]
- } elseif {[string is integer -strict $index]} {
- if {$get_not} {
- lappend INDEX_OPERATIONS listindex-not
- set assign_script [string map [list $index] {
- #not- was specified (already handled not-0)
- set assigned [lreplace $leveldata ]
- }]
- } else {
- lappend INDEX_OPERATIONS listindex
- set assign_script [string map [list $index] {set assigned [lindex $leveldata ]}]
- }
-
- if {$do_bounds_check} {
- if {$index < 0} {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector index_lessthanzero_out_of_bounds_for_all_data_while_bounds_check_on]
- }
- set max [expr {$index + 1}]
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- # bounds_check due to @ directly specified in original index section
- if {${$max} > $len} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- } else {
- ${$assign_script}
- }
- }
- }]
- } else {
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assign_script}
- }
- }]
- }
- } elseif {[string first "end" $index] >=0} {
- if {[regexp {^end([-+]{1,2}[0-9]+)$} $index _match endspec]} {
-
- if {$get_not} {
- lappend INDEX_OPERATIONS listindex-endoffset-not
- set assign_script [string map [list $index] {
- #not- was specified (already handled not-0)
- set assigned [lreplace $leveldata ]
- }]
- } else {
- lappend INDEX_OPERATIONS listindex-endoffset
- set assign_script [string map [list $index ] {set assigned [lindex $leveldata ]}]
- }
-
- if {$do_bounds_check} {
- #tstr won't add braces - so the ${$endspec} value inserted in the expr will remain unbraced as required in this case.
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- set action ?mismatch-not-a-list
- } else {
- #bounds-check is true
- #leave the - from the end- as part of the offset
- set offset [expr ${$endspec}] ;#don't brace!
- if {($offset > 0 || abs($offset) >= $len)} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- } else {
- ${$assign_script}
- }
- }
- }]
- } else {
- append script \n [tstr -ret string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- } else {
- ${$assign_script}
- }
- }]
- }
-
- } elseif {[regexp {^([0-9]+|end|end[-+]{1,2}[0-9]+)-([0-9]+|end|end[-+]{1,2}([0-9]+))$} $index _ start end]} {
- if {$get_not} {
- lappend INDEX_OPERATIONS list-range-not
- set assign_script [string map [list $start $end ] {
- #not- was specified (already handled not-0)
- set assigned [lreplace $leveldata ]
- }]
- } else {
- lappend INDEX_OPERATIONS list-range
- set assign_script [string map [list $start $end] {set assigned [lrange $leveldata ]}]
- }
-
- append script \n [tstr -ret string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- }
- }]
-
- if {$do_bounds_check} {
- if {[string is integer -strict $start]} {
- if {$start < 0} {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector start_lessthanzero_out_of_bounds_for_all_data_while_bounds_check_on]
- }
- append script \n [tstr -return string -allowcommands {
- set start ${$start}
- if {$start+1 > $len} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- }]
- } elseif {$start eq "end"} {
- #noop
- } else {
- set startoffset [string range $start 3 end] ;#include the - from end-
- set startoffset [expr $startoffset] ;#don't brace!
- if {$startoffset > 0} {
- #e.g end+1
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end+x_out_of_bounds_for_all_data_while_bounds_check_on]
-
- }
- append script \n [tstr -return string -allowcommands {
- set startoffset ${$startoffset}
- if {abs($startoffset) >= $len} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- }]
- }
- if {[string is integer -strict $end]} {
- if {$end < 0} {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end_lessthanzero_out_of_bounds_for_all_data_while_bounds_check_on]
- }
- append script \n [tstr -return string -allowcommands {
- set end ${$end}
- if {$end+1 > $len} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- }]
- } elseif {$end eq "end"} {
- #noop
- } else {
- set endoffset [string range $end 3 end] ;#include the - from end-
-
- set endoffset [expr $endoffset] ;#don't brace!
- if {$endoffset > 0} {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end+x_out_of_bounds_for_all_data_while_bounds_check_on]
- }
- append script \n [tstr -return string -allowcommands {
- set endoffset ${$endoffset}
- if {abs($endoffset) >= $len} {
- #set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- }]
- }
- }
-
- append script \n [string map [list $assign_script] {
- if {![string match ?mismatch-* $action]} {
-
- }
- }]
-
- } else {
- #fail now - no need for script
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- } elseif {[string first - $index] > 0} {
- #e.g @1-3 gets here
- #JMN
- if {$get_not} {
- lappend INDEX_OPERATIONS list-range-not
- } else {
- lappend INDEX_OPERATIONS list-range
- }
-
- append script \n [tstr -return string -allowcommands {
- if {[catch {llength $leveldata} len]} {
- #set action ?mismatch-not-a-list
- ${[tstr -ret string $tpl_return_mismatch_not_a_list]}
- }
- }]
-
- #handle pure int-int ranges separately
- set testindex [string map [list - "" + ""] $index]
- if {[string is digit -strict $testindex]} {
- #don't worry about leading - negative value for indices not valid anyway
- set parts [split $index -]
- if {[llength $parts] != 2} {
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- lassign $parts start end
-
- #review - Tcl lrange just returns nothing silently.
- #if we don't intend to implement reverse indexing - we should probably not emit an error
- if {$start > $end} {
- puts stderr "pipesyntax for selector $selector error - reverse index unimplemented"
- error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector]
- }
- if {$do_bounds_check} {
- #append script [string map [list $start $end] {
- # set start
- # set end
- # if {$start+1 > $len || $end+1 > $len} {
- # set action ?mismatch-list-index-out-of-range
- # }
- #}]
- #set eplusone [expr {$end+1}]
- append script [tstr -return string -allowcommands {
- if {$len < ${[expr {$end+1}]}} {
- set action ?mismatch-list-index-out-of-range
- ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]}
- }
- }]
- }
-
-
- if {$get_not} {
- set assign_script [string map [list $start $end] {
- #not- was specified (already handled not-0)
- set assigned [lreplace $leveldata ]
- }]
- } else {
- set assign_script [string map [list $start